deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/task/.eslintrc.js
Normal file
10
packages/task/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
204
packages/task/README.md
Normal file
204
packages/task/README.md
Normal file
@@ -0,0 +1,204 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - TASK EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/task` extension extension permits executing scripts or binaries in the application's backend.
|
||||
|
||||
Tasks launch configurations can be defined independently for each workspace, under `.theia/tasks.json`. When present, they are automatically picked-up when a client opens a workspace, and watches for changes. A task can be executed by triggering the "Run Task" command (shortcut F1). A list of known tasks will then be available, one of which can be selected to trigger execution.
|
||||
|
||||
Each task configuration looks like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"label": "Test task - list workspace files recursively",
|
||||
"type": "shell",
|
||||
"command": "ls",
|
||||
"args": [
|
||||
"-alR"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"dir",
|
||||
"/s"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*label*: a unique string that identifies the task. That's what's shown to the user, when it's time to chose one task configuration to run.
|
||||
|
||||
*type*: determines what type of process will be used to execute the task. Can be "process" or "shell". "Shell" processes' output can be shown in Theia's frontend, in a terminal widget. If type set as "process" then task will be run without their output being shown.
|
||||
|
||||
*command*: the actual command or script to execute. The command can have no path (e.g. "ls") if it can be found in the system path. Else it can have an absolute path, in which case there is no confusion. Or it can have a relative path, in which case it will be interpreted to be relative to cwd. e.g. "./task" would be interpreted to mean a script or binary called "task", right under the workspace root directory.
|
||||
|
||||
*args*: a list of strings, each one being one argument to pass to the command.
|
||||
|
||||
*options*: the command options used when the command is executed. This is the place to provide the
|
||||
|
||||
- *cwd*: the current working directory, in which the task's command will execute. This is the equivalent of doing a "cd" to that directory, on the command-line, before running the command. This can contain the variable *${workspaceFolder}*, which will be replaced at execution time by the path of the current workspace. If left undefined, will by default be set to workspace root.
|
||||
- *env*: the environment of the executed program or shell. If omitted the parent process' environment is used.
|
||||
- *shell*: configuration of the shell when task type is `shell`, where users can specify the shell to use with *shell*, and the arguments to be passed to the shell executable to run in command mode with *args*.
|
||||
|
||||
By default, *command* and *args* above are used on all platforms. However it's not always possible to express a task in the same way, both on Unix and Windows. The command and/or arguments may be different, for example. If a task needs to work on Linux, MacOS, and Windows, it is better to have separated command, command arguments, and options.
|
||||
|
||||
*windows*: if *windows* is defined, its command, command arguments, and options (i.e., *windows.command*, *windows.args*, and *windows.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a Windows backend.
|
||||
|
||||
*osx*: if *osx* is defined, its command, command arguments, and options (i.e., *osx.command*, *osx.args*, and *osx.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a MacOS backend.
|
||||
|
||||
*linux*: if *linux* is defined, its command, command arguments, and options (i.e., *linux.command*, *linux.args*, and *linux.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a Linux backend.
|
||||
|
||||
Here is a sample tasks.json that can be used to test tasks. Just add this content under the theia source directory, in directory `.theia`:
|
||||
|
||||
``` json
|
||||
{
|
||||
// Some sample Theia tasks
|
||||
"tasks": [
|
||||
{
|
||||
"label": "[Task] short running test task (~3s)",
|
||||
"type": "shell",
|
||||
"command": "./task",
|
||||
"args": [
|
||||
"default 1",
|
||||
"default 2",
|
||||
"default 3"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/packages/task/src/node/test-resources/"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"task.bat",
|
||||
"windows abc"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"args": [
|
||||
"linux 1",
|
||||
"linux 2",
|
||||
"linux 3"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[Task] long running test task (~300s)",
|
||||
"type": "shell",
|
||||
"command": "./task-long-running",
|
||||
"args": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/packages/task/src/node/test-resources/"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"task-long-running.bat"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[Task] recursively list files from workspace root",
|
||||
"type": "shell",
|
||||
"command": "ls",
|
||||
"args": [
|
||||
"-alR"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"dir",
|
||||
"/s"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "[Task] Echo a string",
|
||||
"type": "shell",
|
||||
"command": "bash",
|
||||
"args": [
|
||||
"-c",
|
||||
"echo 1 2 3"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Variables substitution
|
||||
|
||||
The variables are supported in the following properties, using `${variableName}` syntax:
|
||||
|
||||
- `command`
|
||||
- `args`
|
||||
- `options.cwd`
|
||||
- `windows.command`
|
||||
- `windows.args`
|
||||
- `windows.options.cwd`
|
||||
- `osx.command`
|
||||
- `osx.args`
|
||||
- `osx.options.cwd`
|
||||
- `linux.command`
|
||||
- `linux.args`
|
||||
- `linux.options.cwd`
|
||||
|
||||
See [here](https://www.theia-ide.org/doc/index.html) for a detailed documentation.
|
||||
|
||||
## Contribution points
|
||||
|
||||
The extension provides contribution points:
|
||||
|
||||
- `browser/TaskContribution` - allows an extension to provide its own Task format and/or to provide the Tasks programmatically to the system
|
||||
|
||||
```typescript
|
||||
export interface TaskContribution {
|
||||
registerResolvers?(resolvers: TaskResolverRegistry): void;
|
||||
registerProviders?(providers: TaskProviderRegistry): void;
|
||||
}
|
||||
```
|
||||
|
||||
- `node/TaskRunnerContribution` - allows an extension to provide its own way of running/killing a Task
|
||||
|
||||
```typescript
|
||||
export interface TaskRunnerContribution {
|
||||
registerRunner(runners: TaskRunnerRegistry): void;
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/task`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_task.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
62
packages/task/package.json
Normal file
62
packages/task/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@theia/task",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Task extension. This extension adds support for executing raw or terminal processes in the backend.",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/editor": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@theia/markers": "1.68.0",
|
||||
"@theia/monaco": "1.68.0",
|
||||
"@theia/monaco-editor-core": "1.96.302",
|
||||
"@theia/process": "1.68.0",
|
||||
"@theia/terminal": "1.68.0",
|
||||
"@theia/userstorage": "1.68.0",
|
||||
"@theia/variable-resolver": "1.68.0",
|
||||
"@theia/workspace": "1.68.0",
|
||||
"async-mutex": "^0.3.1",
|
||||
"jsonc-parser": "^2.2.0",
|
||||
"p-debounce": "^2.1.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/task-frontend-module",
|
||||
"backend": "lib/node/task-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
22
packages/task/src/browser/index.ts
Normal file
22
packages/task/src/browser/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './task-service';
|
||||
export * from './task-contribution';
|
||||
export * from './task-definition-registry';
|
||||
export * from './task-problem-matcher-registry';
|
||||
export * from './task-problem-pattern-registry';
|
||||
export * from './task-schema-updater';
|
||||
@@ -0,0 +1,31 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { ProcessTaskResolver } from './process-task-resolver';
|
||||
import { TaskContribution, TaskResolverRegistry } from '../task-contribution';
|
||||
|
||||
@injectable()
|
||||
export class ProcessTaskContribution implements TaskContribution {
|
||||
|
||||
@inject(ProcessTaskResolver)
|
||||
protected readonly processTaskResolver: ProcessTaskResolver;
|
||||
|
||||
registerResolvers(resolvers: TaskResolverRegistry): void {
|
||||
resolvers.registerExecutionResolver('process', this.processTaskResolver);
|
||||
resolvers.registerExecutionResolver('shell', this.processTaskResolver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { ProcessTaskContribution } from './process-task-contribution';
|
||||
import { ProcessTaskResolver } from './process-task-resolver';
|
||||
import { TaskContribution } from '../task-contribution';
|
||||
|
||||
export function bindProcessTaskModule(bind: interfaces.Bind): void {
|
||||
|
||||
bind(ProcessTaskResolver).toSelf().inSingletonScope();
|
||||
bind(ProcessTaskContribution).toSelf().inSingletonScope();
|
||||
bind(TaskContribution).toService(ProcessTaskContribution);
|
||||
}
|
||||
89
packages/task/src/browser/process/process-task-resolver.ts
Normal file
89
packages/task/src/browser/process/process-task-resolver.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { VariableResolverService } from '@theia/variable-resolver/lib/browser';
|
||||
import { TaskResolver } from '../task-contribution';
|
||||
import { TaskConfiguration } from '../../common/task-protocol';
|
||||
import { ProcessTaskConfiguration } from '../../common/process/task-protocol';
|
||||
import { TaskDefinitionRegistry } from '../task-definition-registry';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class ProcessTaskResolver implements TaskResolver {
|
||||
|
||||
@inject(VariableResolverService)
|
||||
protected readonly variableResolverService: VariableResolverService;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
/**
|
||||
* Perform some adjustments to the task launch configuration, before sending
|
||||
* it to the backend to be executed. We can make sure that parameters that
|
||||
* are optional to the user but required by the server will be defined, with
|
||||
* sane default values. Also, resolve all known variables, e.g. `${workspaceFolder}`.
|
||||
*/
|
||||
async resolveTask(taskConfig: TaskConfiguration): Promise<TaskConfiguration> {
|
||||
const type = taskConfig.executionType || taskConfig.type;
|
||||
if (type !== 'process' && type !== 'shell') {
|
||||
throw new Error('Unsupported task configuration type.');
|
||||
}
|
||||
const context = typeof taskConfig._scope === 'string' ? new URI(taskConfig._scope) : undefined;
|
||||
const variableResolverOptions = {
|
||||
context, configurationSection: 'tasks'
|
||||
};
|
||||
const processTaskConfig = taskConfig as ProcessTaskConfiguration;
|
||||
let cwd = processTaskConfig.options && processTaskConfig.options.cwd;
|
||||
if (!cwd) {
|
||||
const rootURI = this.workspaceService.getWorkspaceRootUri(context);
|
||||
if (rootURI) {
|
||||
cwd = rootURI.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const result: ProcessTaskConfiguration = {
|
||||
...processTaskConfig,
|
||||
command: await this.variableResolverService.resolve(processTaskConfig.command, variableResolverOptions),
|
||||
args: processTaskConfig.args ? await this.variableResolverService.resolve(processTaskConfig.args, variableResolverOptions) : undefined,
|
||||
windows: processTaskConfig.windows ? {
|
||||
command: await this.variableResolverService.resolve(processTaskConfig.windows.command, variableResolverOptions),
|
||||
args: processTaskConfig.windows.args ? await this.variableResolverService.resolve(processTaskConfig.windows.args, variableResolverOptions) : undefined,
|
||||
options: processTaskConfig.windows.options
|
||||
} : undefined,
|
||||
osx: processTaskConfig.osx ? {
|
||||
command: await this.variableResolverService.resolve(processTaskConfig.osx.command, variableResolverOptions),
|
||||
args: processTaskConfig.osx.args ? await this.variableResolverService.resolve(processTaskConfig.osx.args, variableResolverOptions) : undefined,
|
||||
options: processTaskConfig.osx.options
|
||||
} : undefined,
|
||||
linux: processTaskConfig.linux ? {
|
||||
command: await this.variableResolverService.resolve(processTaskConfig.linux.command, variableResolverOptions),
|
||||
args: processTaskConfig.linux.args ? await this.variableResolverService.resolve(processTaskConfig.linux.args, variableResolverOptions) : undefined,
|
||||
options: processTaskConfig.linux.options
|
||||
} : undefined,
|
||||
options: {
|
||||
cwd: await this.variableResolverService.resolve(cwd, variableResolverOptions),
|
||||
env: processTaskConfig.options?.env && await this.variableResolverService.resolve(processTaskConfig.options.env, variableResolverOptions),
|
||||
shell: processTaskConfig.options && processTaskConfig.options.shell
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Container } from '@theia/core/shared/inversify';
|
||||
import { ProvidedTaskConfigurations } from './provided-task-configurations';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import { TaskProviderRegistry } from './task-contribution';
|
||||
import { TaskConfiguration } from '../common';
|
||||
|
||||
describe('provided-task-configurations', () => {
|
||||
let container: Container;
|
||||
beforeEach(() => {
|
||||
container = new Container();
|
||||
container.bind(ProvidedTaskConfigurations).toSelf().inSingletonScope();
|
||||
container.bind(TaskProviderRegistry).toSelf().inSingletonScope();
|
||||
container.bind(TaskDefinitionRegistry).toSelf().inSingletonScope();
|
||||
});
|
||||
|
||||
it('provided-task-search', async () => {
|
||||
const providerRegistry = container.get(TaskProviderRegistry);
|
||||
providerRegistry.register('test', {
|
||||
provideTasks(): Promise<TaskConfiguration[]> {
|
||||
return Promise.resolve([{ type: 'test', label: 'task from test', _source: 'test', _scope: 'test' } as TaskConfiguration]);
|
||||
}
|
||||
});
|
||||
|
||||
const task = await container.get(ProvidedTaskConfigurations).getTask(1, 'test', 'task from test', 'test');
|
||||
assert.isOk(task);
|
||||
assert.equal(task!.type, 'test');
|
||||
assert.equal(task!.label, 'task from test');
|
||||
});
|
||||
});
|
||||
213
packages/task/src/browser/provided-task-configurations.ts
Normal file
213
packages/task/src/browser/provided-task-configurations.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { TaskProviderRegistry, TaskProvider } from './task-contribution';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import { TaskConfiguration, TaskCustomization, TaskOutputPresentation, TaskConfigurationScope, TaskScope } from '../common';
|
||||
|
||||
export const ALL_TASK_TYPES: string = '*';
|
||||
|
||||
@injectable()
|
||||
export class ProvidedTaskConfigurations {
|
||||
/**
|
||||
* Map of source (name of extension, or path of root folder that the task config comes from) and `task config map`.
|
||||
* For the second level of inner map, the key is task label.
|
||||
* For the third level of inner map, the key is the task scope and value TaskConfiguration.
|
||||
*/
|
||||
protected tasksMap = new Map<string, Map<string, Map<string | undefined, TaskConfiguration>>>();
|
||||
|
||||
@inject(TaskProviderRegistry)
|
||||
protected readonly taskProviderRegistry: TaskProviderRegistry;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
private currentToken: number = 0;
|
||||
private activatedProvidersTypes: string[] = [];
|
||||
private nextToken = 1;
|
||||
|
||||
startUserAction(): number {
|
||||
return this.nextToken++;
|
||||
}
|
||||
|
||||
protected updateUserAction(token: number): void {
|
||||
if (this.currentToken !== token) {
|
||||
this.currentToken = token;
|
||||
this.activatedProvidersTypes.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected pushActivatedProvidersType(taskType: string): void {
|
||||
if (!this.activatedProvidersTypes.includes(taskType)) {
|
||||
this.activatedProvidersTypes.push(taskType);
|
||||
}
|
||||
}
|
||||
|
||||
protected isTaskProviderActivationNeeded(taskType?: string): boolean {
|
||||
if (!taskType || this.activatedProvidersTypes.includes(taskType!) || this.activatedProvidersTypes.includes(ALL_TASK_TYPES)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate providers for the given taskType
|
||||
* @param taskType A specific task type or '*' to indicate all task providers
|
||||
*/
|
||||
protected async activateProviders(taskType?: string): Promise<void> {
|
||||
if (!!taskType) {
|
||||
await this.taskProviderRegistry.activateProvider(taskType);
|
||||
this.pushActivatedProvidersType(taskType);
|
||||
}
|
||||
}
|
||||
|
||||
/** returns a list of provided tasks matching an optional given type, or all if '*' is used */
|
||||
async getTasks(token: number, type?: string): Promise<TaskConfiguration[]> {
|
||||
await this.refreshTasks(token, type);
|
||||
const tasks: TaskConfiguration[] = [];
|
||||
for (const taskLabelMap of this.tasksMap!.values()) {
|
||||
for (const taskScopeMap of taskLabelMap.values()) {
|
||||
for (const task of taskScopeMap.values()) {
|
||||
if (!type || task.type === type || type === ALL_TASK_TYPES) {
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
protected async refreshTasks(token: number, taskType?: string): Promise<void> {
|
||||
const newProviderActivationNeeded = this.isTaskProviderActivationNeeded(taskType);
|
||||
if (token !== this.currentToken || newProviderActivationNeeded) {
|
||||
this.updateUserAction(token);
|
||||
await this.activateProviders(taskType);
|
||||
const providers = await this.taskProviderRegistry.getProviders();
|
||||
|
||||
const providedTasks: TaskConfiguration[] = (await Promise.all(providers.map(p => this.resolveTaskConfigurations(p))))
|
||||
.reduce((acc, taskArray) => acc.concat(taskArray), []);
|
||||
this.cacheTasks(providedTasks);
|
||||
}
|
||||
}
|
||||
|
||||
protected async resolveTaskConfigurations(taskProvider: TaskProvider): Promise<TaskConfiguration[]> {
|
||||
return (await taskProvider.provideTasks())
|
||||
// Global/User tasks from providers are not supported.
|
||||
.filter(task => task.scope !== TaskScope.Global)
|
||||
.map(providedTask => {
|
||||
const originalPresentation = providedTask.presentation || {};
|
||||
return {
|
||||
...providedTask,
|
||||
presentation: {
|
||||
...TaskOutputPresentation.getDefault(),
|
||||
...originalPresentation
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** returns the task configuration for a given source and label or undefined if none */
|
||||
async getTask(token: number, source: string, taskLabel: string, scope: TaskConfigurationScope): Promise<TaskConfiguration | undefined> {
|
||||
await this.refreshTasks(token);
|
||||
return this.getCachedTask(source, taskLabel, scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the detected task for the given task customization.
|
||||
* The detected task is considered as a "match" to the task customization if it has all the `required` properties.
|
||||
* In case that more than one customization is found, return the one that has the biggest number of matched properties.
|
||||
*
|
||||
* @param customization the task customization
|
||||
* @return the detected task for the given task customization. If the task customization is not found, `undefined` is returned.
|
||||
*/
|
||||
async getTaskToCustomize(token: number, customization: TaskCustomization, scope: TaskConfigurationScope): Promise<TaskConfiguration | undefined> {
|
||||
const definition = this.taskDefinitionRegistry.getDefinition(customization);
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const matchedTasks: TaskConfiguration[] = [];
|
||||
let highest = -1;
|
||||
const tasks = await this.getTasks(token, customization.type);
|
||||
for (const task of tasks) { // find detected tasks that match the `definition`
|
||||
const required = definition.properties.required || [];
|
||||
if (!required.every(requiredProp => customization[requiredProp] !== undefined)) {
|
||||
continue;
|
||||
}
|
||||
let score = required.length; // number of required properties
|
||||
const requiredProps = new Set(required);
|
||||
// number of optional properties
|
||||
score += definition.properties.all.filter(p => !requiredProps.has(p) && customization[p] !== undefined).length;
|
||||
if (score >= highest) {
|
||||
if (score > highest) {
|
||||
highest = score;
|
||||
matchedTasks.length = 0;
|
||||
}
|
||||
matchedTasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks with scope set to 'Workspace' can be customized in a workspace root, and will not match
|
||||
// providers scope 'TaskScope.Workspace' unless specifically included as below.
|
||||
const scopes = [scope, TaskScope.Workspace];
|
||||
// find the task that matches the `customization`.
|
||||
// The scenario where more than one match is found should not happen unless users manually enter multiple customizations for one type of task
|
||||
// If this does happen, return the first match
|
||||
const matchedTask = matchedTasks.find(t =>
|
||||
scopes.some(scp => scp === t._scope) && definition.properties.all.every(p => t[p] === customization[p])
|
||||
);
|
||||
return matchedTask;
|
||||
}
|
||||
|
||||
protected getCachedTask(source: string, taskLabel: string, scope?: TaskConfigurationScope): TaskConfiguration | undefined {
|
||||
const labelConfigMap = this.tasksMap.get(source);
|
||||
if (labelConfigMap) {
|
||||
const scopeConfigMap = labelConfigMap.get(taskLabel);
|
||||
if (scopeConfigMap) {
|
||||
if (scope) {
|
||||
return scopeConfigMap.get(scope.toString());
|
||||
}
|
||||
return Array.from(scopeConfigMap.values())[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected cacheTasks(tasks: TaskConfiguration[]): void {
|
||||
this.tasksMap.clear();
|
||||
for (const task of tasks) {
|
||||
const label = task.label;
|
||||
const source = task._source;
|
||||
const scope = task._scope;
|
||||
if (this.tasksMap.has(source)) {
|
||||
const labelConfigMap = this.tasksMap.get(source)!;
|
||||
if (labelConfigMap.has(label)) {
|
||||
labelConfigMap.get(label)!.set(scope.toString(), task);
|
||||
} else {
|
||||
const newScopeConfigMap = new Map<undefined | string, TaskConfiguration>();
|
||||
newScopeConfigMap.set(scope.toString(), task);
|
||||
labelConfigMap.set(label, newScopeConfigMap);
|
||||
}
|
||||
} else {
|
||||
const newLabelConfigMap = new Map<string, Map<undefined | string, TaskConfiguration>>();
|
||||
const newScopeConfigMap = new Map<undefined | string, TaskConfiguration>();
|
||||
newScopeConfigMap.set(scope.toString(), task);
|
||||
newLabelConfigMap.set(label, newScopeConfigMap);
|
||||
this.tasksMap.set(source, newLabelConfigMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
842
packages/task/src/browser/quick-open-task.ts
Normal file
842
packages/task/src/browser/quick-open-task.ts
Normal file
@@ -0,0 +1,842 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, optional } from '@theia/core/shared/inversify';
|
||||
import { TaskService } from './task-service';
|
||||
import { TaskInfo, TaskConfiguration, TaskCustomization, TaskScope, TaskConfigurationScope, TaskDefinition } from '../common/task-protocol';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { LabelProvider, QuickAccessProvider, QuickAccessRegistry, QuickInputService, QuickPick } from '@theia/core/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
|
||||
import { ALL_TASK_TYPES } from './provided-task-configurations';
|
||||
import { TaskNameResolver } from './task-name-resolver';
|
||||
import { TaskSourceResolver } from './task-source-resolver';
|
||||
import { TaskConfigurationManager } from './task-configuration-manager';
|
||||
import { filterItems, QuickInputButton, QuickPickItem, QuickPickItemOrSeparator, QuickPicks, QuickPickInput, QuickPickValue } from
|
||||
'@theia/core/lib/browser/quick-input/quick-input-service';
|
||||
import { CancellationToken, PreferenceService } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { TriggerAction } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/browser/pickerQuickAccess';
|
||||
|
||||
export namespace ConfigureTaskAction {
|
||||
export const ID = 'workbench.action.tasks.configureTaskRunner';
|
||||
export const TEXT = nls.localizeByDefault('Configure Task');
|
||||
}
|
||||
|
||||
export type TaskEntry = QuickPickItemOrSeparator | QuickPickValue<string>;
|
||||
export namespace TaskEntry {
|
||||
export function isQuickPickValue(item: QuickPickItemOrSeparator | QuickPickValue<String>): item is QuickPickValue<string> {
|
||||
return 'value' in item && typeof item.value === 'string';
|
||||
}
|
||||
}
|
||||
|
||||
export const CHOOSE_TASK = nls.localizeByDefault('Select the task to run');
|
||||
export const CONFIGURE_A_TASK = nls.localizeByDefault('Configure a Task');
|
||||
export const NO_TASK_TO_RUN = nls.localize('theia/task/noTaskToRun', 'No task to run found. Configure Tasks...');
|
||||
export const NO_TASKS_FOUND = nls.localize('theia/task/noTasksFound', 'No tasks found');
|
||||
export const SHOW_ALL = nls.localizeByDefault('Show All Tasks...');
|
||||
|
||||
@injectable()
|
||||
export class QuickOpenTask implements QuickAccessProvider {
|
||||
static readonly PREFIX = 'task ';
|
||||
readonly description: string = nls.localizeByDefault('Run Task');
|
||||
protected items: Array<TaskEntry> = [];
|
||||
|
||||
@inject(TaskService)
|
||||
protected readonly taskService: TaskService;
|
||||
|
||||
@inject(QuickInputService) @optional()
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(QuickAccessRegistry)
|
||||
protected readonly quickAccessRegistry: QuickAccessRegistry;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(TaskNameResolver)
|
||||
protected readonly taskNameResolver: TaskNameResolver;
|
||||
|
||||
@inject(TaskSourceResolver)
|
||||
protected readonly taskSourceResolver: TaskSourceResolver;
|
||||
|
||||
@inject(TaskConfigurationManager)
|
||||
protected readonly taskConfigurationManager: TaskConfigurationManager;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferences: PreferenceService;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
init(): Promise<void> {
|
||||
return this.doInit(this.taskService.startUserAction());
|
||||
}
|
||||
|
||||
protected async doInit(token: number): Promise<void> {
|
||||
const recentTasks = this.taskService.recentTasks;
|
||||
const configuredTasks = await this.taskService.getConfiguredTasks(token);
|
||||
const providedTypes = this.taskDefinitionRegistry.getAll();
|
||||
|
||||
const { filteredRecentTasks, filteredConfiguredTasks } = this.getFilteredTasks(recentTasks, configuredTasks, []);
|
||||
const isMulti: boolean = this.workspaceService.isMultiRootWorkspaceOpened;
|
||||
this.items = [];
|
||||
|
||||
const filteredRecentTasksItems = this.getItems(filteredRecentTasks, nls.localizeByDefault('recently used tasks'), token, isMulti);
|
||||
const filteredConfiguredTasksItems = this.getItems(filteredConfiguredTasks, nls.localizeByDefault('configured tasks'), token, isMulti, {
|
||||
label: `$(plus) ${CONFIGURE_A_TASK}`,
|
||||
execute: () => this.configure()
|
||||
});
|
||||
const providedTypeItems = this.createProvidedTypeItems(providedTypes);
|
||||
|
||||
this.items.push(
|
||||
...filteredRecentTasksItems,
|
||||
...filteredConfiguredTasksItems,
|
||||
...providedTypeItems
|
||||
);
|
||||
|
||||
if (!this.items.length) {
|
||||
this.items.push(({
|
||||
label: NO_TASK_TO_RUN,
|
||||
execute: () => this.configure()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected createProvidedTypeItems(providedTypes: TaskDefinition[]): TaskEntry[] {
|
||||
const result: TaskEntry[] = [];
|
||||
result.push({ type: 'separator', label: nls.localizeByDefault('contributed') });
|
||||
|
||||
providedTypes.sort((t1, t2) =>
|
||||
t1.taskType.localeCompare(t2.taskType)
|
||||
);
|
||||
|
||||
for (const definition of providedTypes) {
|
||||
const type = definition.taskType;
|
||||
result.push(this.toProvidedTaskTypeEntry(type, `$(folder) ${type}`));
|
||||
}
|
||||
|
||||
result.push(this.toProvidedTaskTypeEntry(SHOW_ALL, SHOW_ALL));
|
||||
return result;
|
||||
}
|
||||
|
||||
protected toProvidedTaskTypeEntry(type: string, label: string): TaskEntry {
|
||||
return {
|
||||
label,
|
||||
value: type,
|
||||
/**
|
||||
* This function is used in the context of a QuickAccessProvider (triggered from the command palette: '?task').
|
||||
* It triggers a call to QuickOpenTask#getPicks,
|
||||
* the 'execute' function below is called when the user selects an entry for a task type which triggers the display of
|
||||
* the second level quick pick.
|
||||
*
|
||||
* Due to the asynchronous resolution of second-level tasks, there may be a delay in showing the quick input widget.
|
||||
*
|
||||
* NOTE: The widget is not delayed in other contexts e.g. by commands (Run Tasks), see the implementation at QuickOpenTask#open
|
||||
*
|
||||
* To improve the performance, we may consider using a `PickerQuickAccessProvider` instead of a `QuickAccessProvider`,
|
||||
* and support providing 'FastAndSlowPicks'.
|
||||
*
|
||||
* TODO: Consider the introduction and exposure of monaco `PickerQuickAccessProvider` and the corresponding refactoring for this and other
|
||||
* users of QuickAccessProvider.
|
||||
*/
|
||||
execute: () => {
|
||||
this.doSecondLevel(type);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected onDidTriggerGearIcon(item: QuickPickItem): void {
|
||||
if (item instanceof TaskRunQuickOpenItem) {
|
||||
this.taskService.configure(item.token, item.task);
|
||||
this.quickInputService.hide();
|
||||
}
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
this.showMultiLevelQuickPick();
|
||||
}
|
||||
|
||||
async showMultiLevelQuickPick(skipInit?: boolean): Promise<void> {
|
||||
if (!skipInit) {
|
||||
await this.init();
|
||||
}
|
||||
const picker: QuickPick<TaskEntry> = this.quickInputService.createQuickPick();
|
||||
picker.placeholder = CHOOSE_TASK;
|
||||
picker.matchOnDescription = true;
|
||||
picker.ignoreFocusOut = false;
|
||||
picker.items = this.items;
|
||||
picker.onDidTriggerItemButton(({ item }) => this.onDidTriggerGearIcon(item));
|
||||
|
||||
const firstLevelTask = await this.doPickerFirstLevel(picker);
|
||||
|
||||
if (!!firstLevelTask && TaskEntry.isQuickPickValue(firstLevelTask)) {
|
||||
// A taskType was selected
|
||||
picker.busy = true;
|
||||
await this.doSecondLevel(firstLevelTask.value);
|
||||
} else if (!!firstLevelTask && 'execute' in firstLevelTask && typeof firstLevelTask.execute === 'function') {
|
||||
firstLevelTask.execute();
|
||||
}
|
||||
picker.dispose();
|
||||
}
|
||||
|
||||
protected async doPickerFirstLevel(picker: QuickPick<TaskEntry>): Promise<TaskEntry | undefined> {
|
||||
picker.show();
|
||||
const firstLevelPickerResult = await new Promise<TaskEntry | undefined | null>(resolve => {
|
||||
picker.onDidAccept(async () => {
|
||||
resolve(picker.selectedItems ? picker.selectedItems[0] : undefined);
|
||||
});
|
||||
});
|
||||
return firstLevelPickerResult ?? undefined;
|
||||
}
|
||||
|
||||
protected async doSecondLevel(taskType: string): Promise<void> {
|
||||
// Resolve Second level tasks based on selected TaskType
|
||||
const isMulti = this.workspaceService.isMultiRootWorkspaceOpened;
|
||||
const token = this.taskService.startUserAction();
|
||||
|
||||
const providedTasks = taskType === SHOW_ALL ?
|
||||
await this.taskService.getProvidedTasks(token, ALL_TASK_TYPES) :
|
||||
await this.taskService.getProvidedTasks(token, taskType);
|
||||
|
||||
const providedTasksItems = this.getItems(providedTasks, taskType + ' tasks', token, isMulti);
|
||||
|
||||
const label = providedTasksItems.length ?
|
||||
nls.localizeByDefault('Go back ↩') :
|
||||
nls.localizeByDefault('No {0} tasks found. Go back ↩', taskType);
|
||||
|
||||
providedTasksItems.push(({
|
||||
label,
|
||||
execute: () => this.showMultiLevelQuickPick(true)
|
||||
}));
|
||||
|
||||
this.quickInputService?.showQuickPick(providedTasksItems, {
|
||||
placeholder: CHOOSE_TASK,
|
||||
onDidTriggerItemButton: ({ item }) => this.onDidTriggerGearIcon(item)
|
||||
});
|
||||
}
|
||||
|
||||
attach(): void {
|
||||
this.items = [];
|
||||
const isMulti: boolean = this.workspaceService.isMultiRootWorkspaceOpened;
|
||||
this.taskService.getRunningTasks().then(tasks => {
|
||||
if (!tasks.length) {
|
||||
this.items.push({
|
||||
label: NO_TASKS_FOUND,
|
||||
});
|
||||
} else {
|
||||
tasks.forEach((task: TaskInfo) => {
|
||||
// can only attach to terminal processes, so only list those
|
||||
if (task.terminalId) {
|
||||
this.items.push(new RunningTaskQuickOpenItem(
|
||||
task,
|
||||
this.taskService,
|
||||
this.taskNameResolver,
|
||||
this.taskSourceResolver,
|
||||
this.taskDefinitionRegistry,
|
||||
this.labelProvider,
|
||||
isMulti,
|
||||
() => this.taskService.attach(task.terminalId!, task)
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.items.length === 0) {
|
||||
this.items.push(({
|
||||
label: NO_TASKS_FOUND
|
||||
}));
|
||||
}
|
||||
this.quickInputService?.showQuickPick(this.items, { placeholder: CHOOSE_TASK });
|
||||
});
|
||||
}
|
||||
|
||||
async configure(): Promise<void> {
|
||||
this.quickInputService?.pick(this.resolveItemsToConfigure(), { placeHolder: nls.localizeByDefault('Select a task to configure') }).
|
||||
then(async item => {
|
||||
if (item && 'execute' in item && typeof item.execute === 'function') {
|
||||
item.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async resolveItemsToConfigure(): Promise<QuickPickInput<QuickPickItemOrSeparator>[]> {
|
||||
const items: Array<QuickPickInput<QuickPickItemOrSeparator>> = [];
|
||||
const isMulti: boolean = this.workspaceService.isMultiRootWorkspaceOpened;
|
||||
const token: number = this.taskService.startUserAction();
|
||||
|
||||
const configuredTasks = await this.taskService.getConfiguredTasks(token);
|
||||
const providedTasks = await this.taskService.getProvidedTasks(token, ALL_TASK_TYPES);
|
||||
|
||||
// check if tasks.json exists. If not, display "Create tasks.json file from template"
|
||||
// If tasks.json exists and empty, display 'Open tasks.json file'
|
||||
const { filteredConfiguredTasks, filteredProvidedTasks } = this.getFilteredTasks([], configuredTasks, providedTasks);
|
||||
const groupedTasks = this.getGroupedTasksByWorkspaceFolder([...filteredConfiguredTasks, ...filteredProvidedTasks]);
|
||||
if (groupedTasks.has(TaskScope.Global.toString())) {
|
||||
const configs = groupedTasks.get(TaskScope.Global.toString())!;
|
||||
this.addConfigurationItems(items, configs, token, isMulti);
|
||||
}
|
||||
if (groupedTasks.has(TaskScope.Workspace.toString())) {
|
||||
const configs = groupedTasks.get(TaskScope.Workspace.toString())!;
|
||||
this.addConfigurationItems(items, configs, token, isMulti);
|
||||
}
|
||||
|
||||
const rootUris = (await this.workspaceService.roots).map(rootStat => rootStat.resource.toString());
|
||||
for (const rootFolder of rootUris) {
|
||||
const folderName = new URI(rootFolder).displayName;
|
||||
if (groupedTasks.has(rootFolder)) {
|
||||
const configs = groupedTasks.get(rootFolder.toString())!;
|
||||
this.addConfigurationItems(items, configs, token, isMulti);
|
||||
} else {
|
||||
const { configUri } = this.preferences.resolve('tasks', [], rootFolder);
|
||||
const existTaskConfigFile = !!configUri;
|
||||
items.push(({
|
||||
label: existTaskConfigFile ? nls.localizeByDefault('Open tasks.json file') : nls.localizeByDefault('Create tasks.json file from template'),
|
||||
execute: () => {
|
||||
setTimeout(() => this.taskConfigurationManager.openConfiguration(rootFolder));
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (items.length > 0) {
|
||||
items.unshift({
|
||||
type: 'separator',
|
||||
label: isMulti ? folderName : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push(({
|
||||
label: NO_TASKS_FOUND
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private addConfigurationItems(items: QuickPickInput<QuickPickItemOrSeparator>[], configs: TaskConfiguration[], token: number, isMulti: boolean): void {
|
||||
items.push(
|
||||
...configs.map(taskConfig => {
|
||||
const item = new TaskConfigureQuickOpenItem(
|
||||
token,
|
||||
taskConfig,
|
||||
this.taskService,
|
||||
this.taskNameResolver,
|
||||
this.workspaceService,
|
||||
isMulti
|
||||
);
|
||||
item['taskDefinitionRegistry'] = this.taskDefinitionRegistry;
|
||||
return item;
|
||||
}).sort((t1, t2) =>
|
||||
t1.label.localeCompare(t2.label)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected getTaskItems(): QuickPickItem[] {
|
||||
return this.items.filter((item): item is QuickPickItem => item.type !== 'separator' && (item as TaskRunQuickOpenItem).task !== undefined);
|
||||
}
|
||||
|
||||
async runBuildOrTestTask(buildOrTestType: 'build' | 'test'): Promise<void> {
|
||||
const shouldRunBuildTask = buildOrTestType === 'build';
|
||||
const token: number = this.taskService.startUserAction();
|
||||
|
||||
await this.doInit(token);
|
||||
|
||||
const taskItems = this.getTaskItems();
|
||||
|
||||
if (taskItems.length > 0) { // the item in `this.items` is not 'No tasks found'
|
||||
const buildOrTestTasks = taskItems.filter((t: TaskRunQuickOpenItem) =>
|
||||
shouldRunBuildTask ? TaskCustomization.isBuildTask(t.task) : TaskCustomization.isTestTask(t.task)
|
||||
);
|
||||
if (buildOrTestTasks.length > 0) { // build / test tasks are defined in the workspace
|
||||
const defaultBuildOrTestTasks = buildOrTestTasks.filter((t: TaskRunQuickOpenItem) =>
|
||||
shouldRunBuildTask ? TaskCustomization.isDefaultBuildTask(t.task) : TaskCustomization.isDefaultTestTask(t.task)
|
||||
);
|
||||
if (defaultBuildOrTestTasks.length === 1) { // run the default build / test task
|
||||
const defaultBuildOrTestTask = defaultBuildOrTestTasks[0];
|
||||
const taskToRun = (defaultBuildOrTestTask as TaskRunQuickOpenItem).task;
|
||||
const scope = taskToRun._scope;
|
||||
|
||||
if (this.taskDefinitionRegistry && !!this.taskDefinitionRegistry.getDefinition(taskToRun)) {
|
||||
this.taskService.run(token, taskToRun.source, taskToRun.label, scope);
|
||||
} else {
|
||||
this.taskService.run(token, taskToRun._source, taskToRun.label, scope);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// if default build / test task is not found, or there are more than one default,
|
||||
// display the list of build /test tasks to let the user decide which to run
|
||||
this.items = buildOrTestTasks;
|
||||
} else { // no build / test tasks, display an action item to configure the build / test task
|
||||
this.items = [({
|
||||
label: shouldRunBuildTask
|
||||
? nls.localizeByDefault('No build task to run found. Configure Build Task...')
|
||||
: nls.localizeByDefault('No test task to run found. Configure Tasks...'),
|
||||
execute: () => {
|
||||
this.doInit(token).then(() => {
|
||||
// update the `tasks.json` file, instead of running the task itself
|
||||
this.items = this.getTaskItems().map((item: TaskRunQuickOpenItem) => new ConfigureBuildOrTestTaskQuickOpenItem(
|
||||
token,
|
||||
item.task,
|
||||
this.taskService,
|
||||
this.workspaceService.isMultiRootWorkspaceOpened,
|
||||
this.taskNameResolver,
|
||||
shouldRunBuildTask,
|
||||
this.taskConfigurationManager,
|
||||
this.taskDefinitionRegistry,
|
||||
this.taskSourceResolver
|
||||
));
|
||||
this.quickInputService?.showQuickPick(this.items, {
|
||||
placeholder: shouldRunBuildTask
|
||||
? nls.localizeByDefault('Select the task to be used as the default build task')
|
||||
: nls.localizeByDefault('Select the task to be used as the default test task')
|
||||
});
|
||||
});
|
||||
}
|
||||
})];
|
||||
}
|
||||
} else { // no tasks are currently present, prompt users if they'd like to configure a task.
|
||||
this.items = [{
|
||||
label: shouldRunBuildTask
|
||||
? nls.localizeByDefault('No build task to run found. Configure Build Task...')
|
||||
: nls.localizeByDefault('No test task to run found. Configure Tasks...'),
|
||||
execute: () => this.configure()
|
||||
}];
|
||||
}
|
||||
|
||||
this.quickInputService?.showQuickPick(this.items, {
|
||||
placeholder: shouldRunBuildTask
|
||||
? nls.localizeByDefault('Select the build task to run')
|
||||
: nls.localizeByDefault('Select the test task to run'),
|
||||
onDidTriggerItemButton: ({ item }) => this.onDidTriggerGearIcon(item)
|
||||
});
|
||||
}
|
||||
|
||||
async getPicks(filter: string, token: CancellationToken): Promise<QuickPicks> {
|
||||
await this.init();
|
||||
return filterItems(this.items, filter);
|
||||
}
|
||||
|
||||
registerQuickAccessProvider(): void {
|
||||
this.quickAccessRegistry.registerQuickAccessProvider({
|
||||
getInstance: () => this,
|
||||
prefix: QuickOpenTask.PREFIX,
|
||||
placeholder: nls.localizeByDefault('Select the task to run'),
|
||||
helpEntries: [{ description: nls.localizeByDefault('Run Task'), needsEditor: false }]
|
||||
});
|
||||
}
|
||||
|
||||
protected getRunningTaskLabel(task: TaskInfo): string {
|
||||
return `Task id: ${task.taskId}, label: ${task.config.label}`;
|
||||
}
|
||||
|
||||
private getItems(tasks: TaskConfiguration[], groupLabel: string, token: number, isMulti: boolean, defaultTask?: TaskEntry):
|
||||
TaskEntry[] {
|
||||
const items: TaskEntry[] = tasks.map(task =>
|
||||
new TaskRunQuickOpenItem(token, task, this.taskService, isMulti, this.taskDefinitionRegistry, this.taskNameResolver,
|
||||
this.taskSourceResolver, this.taskConfigurationManager, [{
|
||||
iconClass: 'codicon-gear',
|
||||
tooltip: nls.localizeByDefault('Configure Task'),
|
||||
}])
|
||||
).sort((t1, t2) => {
|
||||
let result = (t1.description ?? '').localeCompare(t2.description ?? '');
|
||||
if (result === 0) {
|
||||
result = t1.label.localeCompare(t2.label);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
if (items.length === 0 && defaultTask) {
|
||||
items.push(defaultTask);
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
items.unshift({ type: 'separator', label: groupLabel });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private getFilteredTasks(recentTasks: TaskConfiguration[], configuredTasks: TaskConfiguration[], providedTasks: TaskConfiguration[]): {
|
||||
filteredRecentTasks: TaskConfiguration[], filteredConfiguredTasks: TaskConfiguration[], filteredProvidedTasks: TaskConfiguration[]
|
||||
} {
|
||||
|
||||
const filteredRecentTasks: TaskConfiguration[] = [];
|
||||
recentTasks.forEach(recent => {
|
||||
const originalTaskConfig = [...configuredTasks, ...providedTasks].find(t => this.taskDefinitionRegistry.compareTasks(recent, t));
|
||||
if (originalTaskConfig) {
|
||||
filteredRecentTasks.push(originalTaskConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredProvidedTasks: TaskConfiguration[] = [];
|
||||
providedTasks.forEach(provided => {
|
||||
const exist = [...filteredRecentTasks, ...configuredTasks].some(t => this.taskDefinitionRegistry.compareTasks(provided, t));
|
||||
if (!exist) {
|
||||
filteredProvidedTasks.push(provided);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredConfiguredTasks: TaskConfiguration[] = [];
|
||||
configuredTasks.forEach(configured => {
|
||||
const exist = filteredRecentTasks.some(t => this.taskDefinitionRegistry.compareTasks(configured, t));
|
||||
if (!exist) {
|
||||
filteredConfiguredTasks.push(configured);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
filteredRecentTasks, filteredConfiguredTasks, filteredProvidedTasks
|
||||
};
|
||||
}
|
||||
|
||||
private getGroupedTasksByWorkspaceFolder(tasks: TaskConfiguration[]): Map<string, TaskConfiguration[]> {
|
||||
const grouped = new Map<string, TaskConfiguration[]>();
|
||||
for (const task of tasks) {
|
||||
const scope = task._scope;
|
||||
if (grouped.has(scope.toString())) {
|
||||
grouped.get(scope.toString())!.push(task);
|
||||
} else {
|
||||
grouped.set(scope.toString(), [task]);
|
||||
}
|
||||
}
|
||||
for (const taskConfigs of grouped.values()) {
|
||||
taskConfigs.sort((t1, t2) => t1.label.localeCompare(t2.label));
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
}
|
||||
|
||||
export class TaskRunQuickOpenItem implements QuickPickItem {
|
||||
constructor(
|
||||
readonly token: number,
|
||||
readonly task: TaskConfiguration,
|
||||
protected taskService: TaskService,
|
||||
protected isMulti: boolean,
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry,
|
||||
protected readonly taskNameResolver: TaskNameResolver,
|
||||
protected readonly taskSourceResolver: TaskSourceResolver,
|
||||
protected taskConfigurationManager: TaskConfigurationManager,
|
||||
readonly buttons?: Array<QuickInputButton>
|
||||
) { }
|
||||
|
||||
get label(): string {
|
||||
return this.taskNameResolver.resolve(this.task);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return renderScope(this.task._scope, this.isMulti);
|
||||
}
|
||||
|
||||
get detail(): string | undefined {
|
||||
return this.task.detail;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const scope = this.task._scope;
|
||||
if (this.taskDefinitionRegistry && !!this.taskDefinitionRegistry.getDefinition(this.task)) {
|
||||
this.taskService.run(this.token, this.task.source || this.task._source, this.task.label, scope);
|
||||
} else {
|
||||
this.taskService.run(this.token, this.task._source, this.task.label, scope);
|
||||
}
|
||||
}
|
||||
|
||||
trigger(): TriggerAction {
|
||||
this.taskService.configure(this.token, this.task);
|
||||
return TriggerAction.CLOSE_PICKER;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigureBuildOrTestTaskQuickOpenItem extends TaskRunQuickOpenItem {
|
||||
constructor(
|
||||
token: number,
|
||||
task: TaskConfiguration,
|
||||
taskService: TaskService,
|
||||
isMulti: boolean,
|
||||
taskNameResolver: TaskNameResolver,
|
||||
protected readonly isBuildTask: boolean,
|
||||
taskConfigurationManager: TaskConfigurationManager,
|
||||
taskDefinitionRegistry: TaskDefinitionRegistry,
|
||||
taskSourceResolver: TaskSourceResolver
|
||||
) {
|
||||
super(token, task, taskService, isMulti, taskDefinitionRegistry, taskNameResolver, taskSourceResolver, taskConfigurationManager);
|
||||
}
|
||||
|
||||
override execute(): void {
|
||||
this.taskService.updateTaskConfiguration(this.token, this.task, { group: { kind: this.isBuildTask ? 'build' : 'test', isDefault: true } })
|
||||
.then(() => {
|
||||
if (this.task._scope) {
|
||||
this.taskConfigurationManager.openConfiguration(this.task._scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderScope(scope: TaskConfigurationScope, isMulti: boolean): string {
|
||||
if (typeof scope === 'string') {
|
||||
if (isMulti) {
|
||||
return new URI(scope).displayName;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return TaskScope[scope];
|
||||
}
|
||||
}
|
||||
|
||||
export class TaskConfigureQuickOpenItem implements QuickPickItem {
|
||||
|
||||
protected taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
constructor(
|
||||
protected readonly token: number,
|
||||
protected readonly task: TaskConfiguration,
|
||||
protected readonly taskService: TaskService,
|
||||
protected readonly taskNameResolver: TaskNameResolver,
|
||||
protected readonly workspaceService: WorkspaceService,
|
||||
protected readonly isMulti: boolean
|
||||
) {
|
||||
const stat = this.workspaceService.workspace;
|
||||
this.isMulti = stat ? !stat.isDirectory : false;
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this.taskNameResolver.resolve(this.task);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return renderScope(this.task._scope, this.isMulti);
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
this.execute();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.taskService.configure(this.token, this.task);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class TaskTerminateQuickOpen {
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(QuickInputService) @optional()
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(TaskNameResolver)
|
||||
protected readonly taskNameResolver: TaskNameResolver;
|
||||
|
||||
@inject(TaskSourceResolver)
|
||||
protected readonly taskSourceResolver: TaskSourceResolver;
|
||||
|
||||
@inject(TaskService)
|
||||
protected readonly taskService: TaskService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
async getItems(): Promise<Array<QuickPickItem>> {
|
||||
const items: Array<QuickPickItem> = [];
|
||||
const runningTasks: TaskInfo[] = await this.taskService.getRunningTasks();
|
||||
const isMulti: boolean = this.workspaceService.isMultiRootWorkspaceOpened;
|
||||
if (runningTasks.length <= 0) {
|
||||
items.push(({
|
||||
label: nls.localizeByDefault('No task is currently running'),
|
||||
}));
|
||||
} else {
|
||||
runningTasks.forEach((task: TaskInfo) => {
|
||||
items.push(new RunningTaskQuickOpenItem(
|
||||
task,
|
||||
this.taskService,
|
||||
this.taskNameResolver,
|
||||
this.taskSourceResolver,
|
||||
this.taskDefinitionRegistry,
|
||||
this.labelProvider,
|
||||
isMulti,
|
||||
() => this.taskService.kill(task.taskId)
|
||||
));
|
||||
});
|
||||
if (runningTasks.length > 1) {
|
||||
items.push(({
|
||||
label: nls.localizeByDefault('All Running Tasks'),
|
||||
execute: () => {
|
||||
runningTasks.forEach((t: TaskInfo) => {
|
||||
this.taskService.kill(t.taskId);
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
const items = await this.getItems();
|
||||
this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select a task to terminate') });
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class TaskRunningQuickOpen {
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(QuickInputService) @optional()
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(TaskNameResolver)
|
||||
protected readonly taskNameResolver: TaskNameResolver;
|
||||
|
||||
@inject(TaskSourceResolver)
|
||||
protected readonly taskSourceResolver: TaskSourceResolver;
|
||||
|
||||
@inject(TaskService)
|
||||
protected readonly taskService: TaskService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(TerminalService)
|
||||
protected readonly terminalService: TerminalService;
|
||||
|
||||
async getItems(): Promise<Array<QuickPickItem>> {
|
||||
const items: Array<QuickPickItem> = [];
|
||||
const runningTasks: TaskInfo[] = await this.taskService.getRunningTasks();
|
||||
const isMulti: boolean = this.workspaceService.isMultiRootWorkspaceOpened;
|
||||
if (runningTasks.length <= 0) {
|
||||
items.push(({
|
||||
label: nls.localizeByDefault('No task is currently running'),
|
||||
}));
|
||||
} else {
|
||||
runningTasks.forEach((task: TaskInfo) => {
|
||||
items.push(new RunningTaskQuickOpenItem(
|
||||
task,
|
||||
this.taskService,
|
||||
this.taskNameResolver,
|
||||
this.taskSourceResolver,
|
||||
this.taskDefinitionRegistry,
|
||||
this.labelProvider,
|
||||
isMulti,
|
||||
() => {
|
||||
if (task.terminalId) {
|
||||
const terminal = this.terminalService.getByTerminalId(task.terminalId);
|
||||
if (terminal) {
|
||||
this.terminalService.open(terminal);
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
const items = await this.getItems();
|
||||
this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select the task to show its output') });
|
||||
}
|
||||
}
|
||||
|
||||
export class RunningTaskQuickOpenItem implements QuickPickItem {
|
||||
constructor(
|
||||
protected readonly taskInfo: TaskInfo,
|
||||
protected readonly taskService: TaskService,
|
||||
protected readonly taskNameResolver: TaskNameResolver,
|
||||
protected readonly taskSourceResolver: TaskSourceResolver,
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry,
|
||||
protected readonly labelProvider: LabelProvider,
|
||||
protected readonly isMulti: boolean,
|
||||
public readonly execute: () => void,
|
||||
) { }
|
||||
|
||||
get label(): string {
|
||||
return this.taskNameResolver.resolve(this.taskInfo.config);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return renderScope(this.taskInfo.config._scope, this.isMulti);
|
||||
}
|
||||
|
||||
get detail(): string | undefined {
|
||||
return this.taskInfo.config.detail;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class TaskRestartRunningQuickOpen {
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(QuickInputService) @optional()
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(TaskNameResolver)
|
||||
protected readonly taskNameResolver: TaskNameResolver;
|
||||
|
||||
@inject(TaskSourceResolver)
|
||||
protected readonly taskSourceResolver: TaskSourceResolver;
|
||||
|
||||
@inject(TaskService)
|
||||
protected readonly taskService: TaskService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
async getItems(): Promise<Array<QuickPickItem>> {
|
||||
const items: Array<QuickPickItem> = [];
|
||||
const runningTasks: TaskInfo[] = await this.taskService.getRunningTasks();
|
||||
const isMulti: boolean = this.workspaceService.isMultiRootWorkspaceOpened;
|
||||
if (runningTasks.length <= 0) {
|
||||
items.push({
|
||||
label: nls.localizeByDefault('No task to restart')
|
||||
});
|
||||
} else {
|
||||
runningTasks.forEach((task: TaskInfo) => {
|
||||
items.push(new RunningTaskQuickOpenItem(
|
||||
task,
|
||||
this.taskService,
|
||||
this.taskNameResolver,
|
||||
this.taskSourceResolver,
|
||||
this.taskDefinitionRegistry,
|
||||
this.labelProvider,
|
||||
isMulti,
|
||||
() => this.taskService.restartTask(task)
|
||||
));
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
const items = await this.getItems();
|
||||
this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select the task to restart') });
|
||||
}
|
||||
}
|
||||
19
packages/task/src/browser/style/index.css
Normal file
19
packages/task/src/browser/style/index.css
Normal file
@@ -0,0 +1,19 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.quick-open-task-configure {
|
||||
margin-top: 3px !important;
|
||||
}
|
||||
251
packages/task/src/browser/task-configuration-manager.ts
Normal file
251
packages/task/src/browser/task-configuration-manager.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as jsoncparser from 'jsonc-parser';
|
||||
import debounce = require('p-debounce');
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { PreferenceScope, PreferenceService, DisposableCollection, PreferenceProviderProvider, nls } from '@theia/core/lib/common';
|
||||
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { TaskConfigurationModel } from './task-configuration-model';
|
||||
import { TaskTemplateSelector } from './task-templates';
|
||||
import { TaskCustomization, TaskConfiguration, TaskConfigurationScope, TaskScope } from '../common/task-protocol';
|
||||
import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
|
||||
import { PreferenceConfigurations } from '@theia/core/lib/common/preferences/preference-configurations';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { TaskSchemaUpdater } from './task-schema-updater';
|
||||
import { JSONObject } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { PreferenceProvider } from '@theia/core/lib/common/preferences/preference-provider';
|
||||
|
||||
export interface TasksChange {
|
||||
scope: TaskConfigurationScope;
|
||||
type: FileChangeType;
|
||||
}
|
||||
/**
|
||||
* This class connects the the "tasks" preferences sections to task system: it collects tasks preference values and
|
||||
* provides them to the task system as raw, parsed JSON.
|
||||
*/
|
||||
@injectable()
|
||||
export class TaskConfigurationManager {
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(QuickPickService)
|
||||
protected readonly quickPickService: QuickPickService;
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(TaskSchemaUpdater)
|
||||
protected readonly taskSchemaProvider: TaskSchemaUpdater;
|
||||
|
||||
@inject(PreferenceProviderProvider)
|
||||
protected readonly preferenceProviderProvider: PreferenceProviderProvider;
|
||||
|
||||
@inject(PreferenceConfigurations)
|
||||
protected readonly preferenceConfigurations: PreferenceConfigurations;
|
||||
|
||||
@inject(WorkspaceVariableContribution)
|
||||
protected readonly workspaceVariables: WorkspaceVariableContribution;
|
||||
|
||||
@inject(TaskTemplateSelector)
|
||||
protected readonly taskTemplateSelector: TaskTemplateSelector;
|
||||
|
||||
protected readonly onDidChangeTaskConfigEmitter = new Emitter<TasksChange>();
|
||||
readonly onDidChangeTaskConfig: Event<TasksChange> = this.onDidChangeTaskConfigEmitter.event;
|
||||
|
||||
protected readonly models = new Map<TaskConfigurationScope, TaskConfigurationModel>();
|
||||
protected workspaceDelegate: PreferenceProvider | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.createModels();
|
||||
this.preferenceProviderProvider(PreferenceScope.Folder)?.onDidPreferencesChanged(e => {
|
||||
if (e['tasks']) {
|
||||
this.updateModels();
|
||||
}
|
||||
});
|
||||
this.workspaceService.onWorkspaceChanged(() => {
|
||||
this.updateModels();
|
||||
});
|
||||
this.workspaceService.onWorkspaceLocationChanged(() => {
|
||||
this.updateModels();
|
||||
});
|
||||
}
|
||||
|
||||
protected createModels(): void {
|
||||
const userModel = new TaskConfigurationModel(TaskScope.Global, this.preferenceProviderProvider(PreferenceScope.User));
|
||||
userModel.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ scope: TaskScope.Global, type: FileChangeType.UPDATED }));
|
||||
this.models.set(TaskScope.Global, userModel);
|
||||
|
||||
this.updateModels();
|
||||
}
|
||||
|
||||
protected updateModels = debounce(async () => {
|
||||
const roots = await this.workspaceService.roots;
|
||||
const toDelete = new Set(
|
||||
[...this.models.keys()]
|
||||
.filter(key => key !== TaskScope.Global && key !== TaskScope.Workspace)
|
||||
);
|
||||
this.updateWorkspaceModel();
|
||||
for (const rootStat of roots) {
|
||||
const key = rootStat.resource.toString();
|
||||
toDelete.delete(key);
|
||||
if (!this.models.has(key)) {
|
||||
const model = new TaskConfigurationModel(key, this.preferenceProviderProvider(PreferenceScope.Folder));
|
||||
model.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ scope: key, type: FileChangeType.UPDATED }));
|
||||
model.onDispose(() => this.models.delete(key));
|
||||
this.models.set(key, model);
|
||||
this.onDidChangeTaskConfigEmitter.fire({ scope: key, type: FileChangeType.UPDATED });
|
||||
}
|
||||
}
|
||||
for (const uri of toDelete) {
|
||||
const model = this.models.get(uri);
|
||||
if (model) {
|
||||
model.dispose();
|
||||
}
|
||||
this.onDidChangeTaskConfigEmitter.fire({ scope: uri, type: FileChangeType.DELETED });
|
||||
}
|
||||
}, 500);
|
||||
|
||||
getTasks(scope: TaskConfigurationScope): (TaskCustomization | TaskConfiguration)[] {
|
||||
return this.getModel(scope)?.configurations ?? [];
|
||||
}
|
||||
|
||||
getTask(name: string, scope: TaskConfigurationScope): TaskCustomization | TaskConfiguration | undefined {
|
||||
return this.getTasks(scope).find((configuration: TaskCustomization | TaskConfiguration) => configuration.name === name);
|
||||
}
|
||||
|
||||
async openConfiguration(scope: TaskConfigurationScope): Promise<void> {
|
||||
const taskPrefModel = this.getModel(scope);
|
||||
const maybeURI = typeof scope === 'string' ? scope : undefined;
|
||||
const configURI = this.preferenceService.getConfigUri(this.getMatchingPreferenceScope(scope), maybeURI, 'tasks');
|
||||
if (taskPrefModel && configURI) {
|
||||
await this.doOpen(taskPrefModel, configURI);
|
||||
}
|
||||
}
|
||||
|
||||
async addTaskConfiguration(scope: TaskConfigurationScope, taskConfig: TaskCustomization): Promise<boolean> {
|
||||
const taskPrefModel = this.getModel(scope);
|
||||
if (taskPrefModel) {
|
||||
const configurations = taskPrefModel.configurations;
|
||||
return this.setTaskConfigurations(scope, [...configurations, taskConfig]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async setTaskConfigurations(scope: TaskConfigurationScope, taskConfigs: (TaskCustomization | TaskConfiguration)[]): Promise<boolean> {
|
||||
const taskPrefModel = this.getModel(scope);
|
||||
if (taskPrefModel) {
|
||||
return taskPrefModel.setConfigurations(taskConfigs);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected getModel(scope: TaskConfigurationScope): TaskConfigurationModel | undefined {
|
||||
return this.models.get(scope);
|
||||
}
|
||||
|
||||
protected async doOpen(model: TaskConfigurationModel, configURI: URI): Promise<EditorWidget | undefined> {
|
||||
if (!model.uri) {
|
||||
// The file has not yet been created.
|
||||
await this.doCreate(model, configURI);
|
||||
}
|
||||
return this.editorManager.open(configURI, {
|
||||
mode: 'activate'
|
||||
});
|
||||
}
|
||||
|
||||
protected async doCreate(model: TaskConfigurationModel, configURI: URI): Promise<void> {
|
||||
const content = await this.getInitialConfigurationContent();
|
||||
if (content) {
|
||||
// All scopes but workspace.
|
||||
if (this.preferenceConfigurations.getName(configURI) === 'tasks') {
|
||||
await this.fileService.write(configURI, content);
|
||||
} else {
|
||||
let taskContent: object;
|
||||
try {
|
||||
taskContent = jsoncparser.parse(content);
|
||||
} catch {
|
||||
taskContent = this.taskSchemaProvider.getTaskSchema().default ?? {};
|
||||
}
|
||||
await model.preferences?.setPreference('tasks', taskContent as JSONObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getMatchingPreferenceScope(scope: TaskConfigurationScope): PreferenceScope {
|
||||
switch (scope) {
|
||||
case TaskScope.Global:
|
||||
return PreferenceScope.User;
|
||||
case TaskScope.Workspace:
|
||||
return PreferenceScope.Workspace;
|
||||
default:
|
||||
return PreferenceScope.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
protected async getInitialConfigurationContent(): Promise<string | undefined> {
|
||||
const selected = await this.quickPickService.show(this.taskTemplateSelector.selectTemplates(), {
|
||||
placeholder: nls.localizeByDefault('Select a Task Template')
|
||||
});
|
||||
if (selected) {
|
||||
return selected.value?.content;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly toDisposeOnDelegateChange = new DisposableCollection();
|
||||
protected updateWorkspaceModel(): void {
|
||||
const isFolderWorkspace = this.workspaceService.opened && !this.workspaceService.saved;
|
||||
const newDelegate = isFolderWorkspace ? this.preferenceProviderProvider(PreferenceScope.Folder) : this.preferenceProviderProvider(PreferenceScope.Workspace);
|
||||
const effectiveScope = isFolderWorkspace ? this.workspaceService.tryGetRoots()[0]?.resource.toString() : TaskScope.Workspace;
|
||||
if (newDelegate !== this.workspaceDelegate) {
|
||||
this.workspaceDelegate = newDelegate;
|
||||
this.toDisposeOnDelegateChange.dispose();
|
||||
|
||||
const workspaceModel = new TaskConfigurationModel(effectiveScope, newDelegate);
|
||||
this.toDisposeOnDelegateChange.push(workspaceModel);
|
||||
// If the delegate is the folder preference provider, its events will be relayed via the folder scope models.
|
||||
if (newDelegate === this.preferenceProviderProvider(PreferenceScope.Workspace)) {
|
||||
this.toDisposeOnDelegateChange.push(workspaceModel.onDidChange(() => {
|
||||
this.onDidChangeTaskConfigEmitter.fire({ scope: TaskScope.Workspace, type: FileChangeType.UPDATED });
|
||||
}));
|
||||
}
|
||||
this.models.set(TaskScope.Workspace, workspaceModel);
|
||||
this.onDidChangeTaskConfigEmitter.fire({ scope: effectiveScope, type: FileChangeType.UPDATED });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace TaskConfigurationManager {
|
||||
export interface Data {
|
||||
current?: {
|
||||
name: string
|
||||
workspaceFolderUri?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
104
packages/task/src/browser/task-configuration-model.ts
Normal file
104
packages/task/src/browser/task-configuration-model.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { TaskCustomization, TaskConfiguration, TaskConfigurationScope } from '../common/task-protocol';
|
||||
import { PreferenceProviderDataChanges, PreferenceProviderDataChange, isObject } from '@theia/core/lib/common';
|
||||
import { PreferenceProvider } from '@theia/core/lib/common/preferences/preference-provider';
|
||||
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
||||
|
||||
/**
|
||||
* Holds the task configurations associated with a particular file. Uses an editor model to facilitate
|
||||
* non-destructive editing and coordination with editing the file by hand.
|
||||
*/
|
||||
export class TaskConfigurationModel implements Disposable {
|
||||
|
||||
protected json: TaskConfigurationModel.JsonContent;
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeEmitter
|
||||
);
|
||||
|
||||
constructor(
|
||||
protected readonly scope: TaskConfigurationScope,
|
||||
readonly preferences: PreferenceProvider | undefined
|
||||
) {
|
||||
this.reconcile();
|
||||
if (this.preferences) {
|
||||
this.toDispose.push(this.preferences.onDidPreferencesChanged((e: PreferenceProviderDataChanges) => {
|
||||
const change = e['tasks'];
|
||||
if (change && PreferenceProviderDataChange.affects(change, this.getWorkspaceFolder())) {
|
||||
this.reconcile();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
get uri(): URI | undefined {
|
||||
return this.json.uri;
|
||||
}
|
||||
|
||||
getWorkspaceFolder(): string | undefined {
|
||||
return typeof this.scope === 'string' ? this.scope : undefined;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
get onDispose(): Event<void> {
|
||||
return this.toDispose.onDispose;
|
||||
}
|
||||
|
||||
get configurations(): (TaskCustomization | TaskConfiguration)[] {
|
||||
return this.json.configurations;
|
||||
}
|
||||
|
||||
protected reconcile(): void {
|
||||
this.json = this.parseConfigurations();
|
||||
this.onDidChangeEmitter.fire(undefined);
|
||||
}
|
||||
|
||||
async setConfigurations(value: JSONValue): Promise<boolean> {
|
||||
return this.preferences?.setPreference('tasks.tasks', value, this.getWorkspaceFolder()) || false;
|
||||
}
|
||||
|
||||
protected parseConfigurations(): TaskConfigurationModel.JsonContent {
|
||||
const configurations: (TaskCustomization | TaskConfiguration)[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const res = this.preferences?.resolve<any>('tasks', this.getWorkspaceFolder());
|
||||
if (isObject(res?.value) && Array.isArray(res.value.tasks)) {
|
||||
for (const taskConfig of res.value.tasks) {
|
||||
configurations.push(taskConfig);
|
||||
}
|
||||
}
|
||||
return {
|
||||
uri: res?.configUri,
|
||||
configurations
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
export namespace TaskConfigurationModel {
|
||||
export interface JsonContent {
|
||||
uri?: URI;
|
||||
configurations: (TaskCustomization | TaskConfiguration)[];
|
||||
}
|
||||
}
|
||||
508
packages/task/src/browser/task-configurations.ts
Normal file
508
packages/task/src/browser/task-configurations.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017-2018 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
TaskConfiguration,
|
||||
TaskCustomization,
|
||||
TaskDefinition,
|
||||
TaskOutputPresentation,
|
||||
TaskConfigurationScope,
|
||||
TaskScope,
|
||||
asVariableName
|
||||
} from '../common';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import { ProvidedTaskConfigurations } from './provided-task-configurations';
|
||||
import { TaskConfigurationManager, TasksChange } from './task-configuration-manager';
|
||||
import { TaskSchemaUpdater } from './task-schema-updater';
|
||||
import { TaskSourceResolver } from './task-source-resolver';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
|
||||
export interface TaskConfigurationClient {
|
||||
/**
|
||||
* The task configuration file has changed, so a client might want to refresh its configurations
|
||||
* @returns an array of strings, each one being a task label
|
||||
*/
|
||||
taskConfigurationChanged: (event: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches a tasks.json configuration file and provides a parsed version of the contained task configurations
|
||||
*/
|
||||
@injectable()
|
||||
export class TaskConfigurations implements Disposable {
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
/**
|
||||
* Map of source (path of root folder that the task configs come from) and task config map.
|
||||
* For the inner map (i.e., task config map), the key is task label and value TaskConfiguration
|
||||
*/
|
||||
protected tasksMap = new Map<string, Map<string, TaskConfiguration>>();
|
||||
/**
|
||||
* Map of source (path of root folder that the task configs come from) and task customizations map.
|
||||
*/
|
||||
protected taskCustomizationMap = new Map<string, TaskCustomization[]>();
|
||||
|
||||
protected client: TaskConfigurationClient | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Map of source (path of root folder that the task configs come from) and raw task configurations / customizations.
|
||||
* This map is used to store the data from `tasks.json` files in workspace.
|
||||
*/
|
||||
private rawTaskConfigurations = new Map<string, TaskCustomization[]>();
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(ProvidedTaskConfigurations)
|
||||
protected readonly providedTaskConfigurations: ProvidedTaskConfigurations;
|
||||
|
||||
@inject(TaskConfigurationManager)
|
||||
protected readonly taskConfigurationManager: TaskConfigurationManager;
|
||||
|
||||
@inject(TaskSchemaUpdater)
|
||||
protected readonly taskSchemaUpdater: TaskSchemaUpdater;
|
||||
|
||||
@inject(TaskSourceResolver)
|
||||
protected readonly taskSourceResolver: TaskSourceResolver;
|
||||
|
||||
constructor() {
|
||||
this.toDispose.push(Disposable.create(() => {
|
||||
this.tasksMap.clear();
|
||||
this.taskCustomizationMap.clear();
|
||||
this.rawTaskConfigurations.clear();
|
||||
this.client = undefined;
|
||||
}));
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.push(
|
||||
this.taskConfigurationManager.onDidChangeTaskConfig(async change => {
|
||||
try {
|
||||
await this.onDidTaskFileChange([change]);
|
||||
if (this.client) {
|
||||
this.client.taskConfigurationChanged(this.getTaskLabels());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.reorganizeTasks();
|
||||
this.toDispose.push(this.taskSchemaUpdater.onDidChangeTaskSchema(() => this.reorganizeTasks()));
|
||||
}
|
||||
|
||||
setClient(client: TaskConfigurationClient): void {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
/** returns the list of known task labels */
|
||||
getTaskLabels(): string[] {
|
||||
return Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.keys())), [] as string[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a collection of known tasks, which includes:
|
||||
* - all the configured tasks in `tasks.json`, and
|
||||
* - the customized detected tasks.
|
||||
*
|
||||
* The invalid task configs are not returned.
|
||||
*/
|
||||
async getTasks(token: number): Promise<TaskConfiguration[]> {
|
||||
const configuredTasks = Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.values())), [] as TaskConfiguration[]);
|
||||
const detectedTasksAsConfigured: TaskConfiguration[] = [];
|
||||
for (const [rootFolder, customizations] of Array.from(this.taskCustomizationMap.entries())) {
|
||||
for (const customization of customizations) {
|
||||
// TODO: getTasksToCustomize() will ask all task providers to contribute tasks. Doing this in a loop is bad.
|
||||
const detected = await this.providedTaskConfigurations.getTaskToCustomize(token, customization, rootFolder);
|
||||
if (detected) {
|
||||
// there might be a provided task that has a different scope from the task we're inspecting
|
||||
detectedTasksAsConfigured.push({ ...detected, ...customization });
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...configuredTasks, ...detectedTasksAsConfigured];
|
||||
}
|
||||
|
||||
getRawTaskConfigurations(scope?: TaskConfigurationScope): (TaskCustomization | TaskConfiguration)[] {
|
||||
if (scope === undefined) {
|
||||
const tasks: (TaskCustomization | TaskConfiguration)[] = [];
|
||||
for (const configs of this.rawTaskConfigurations.values()) {
|
||||
tasks.push(...configs);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
const scopeKey = this.getKeyFromScope(scope);
|
||||
if (this.rawTaskConfigurations.has(scopeKey)) {
|
||||
return Array.from(this.rawTaskConfigurations.get(scopeKey)!.values());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a collection of invalid task configs as per the task schema defined in Theia.
|
||||
*/
|
||||
getInvalidTaskConfigurations(): (TaskCustomization | TaskConfiguration)[] {
|
||||
const invalidTaskConfigs: (TaskCustomization | TaskConfiguration)[] = [];
|
||||
for (const taskConfigs of this.rawTaskConfigurations.values()) {
|
||||
for (const taskConfig of taskConfigs) {
|
||||
const isValid = this.isTaskConfigValid(taskConfig);
|
||||
if (!isValid) {
|
||||
invalidTaskConfigs.push(taskConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
return invalidTaskConfigs;
|
||||
}
|
||||
|
||||
/** returns the task configuration for a given label or undefined if none */
|
||||
getTask(scope: TaskConfigurationScope, taskLabel: string): TaskConfiguration | undefined {
|
||||
const labelConfigMap = this.tasksMap.get(this.getKeyFromScope(scope));
|
||||
if (labelConfigMap) {
|
||||
return labelConfigMap.get(taskLabel);
|
||||
}
|
||||
}
|
||||
|
||||
/** returns the customized task for a given label or undefined if none */
|
||||
async getCustomizedTask(token: number, scope: TaskConfigurationScope, taskLabel: string): Promise<TaskConfiguration | undefined> {
|
||||
const customizations = this.taskCustomizationMap.get(this.getKeyFromScope(scope));
|
||||
if (customizations) {
|
||||
const customization = customizations.find(cus => cus.label === taskLabel);
|
||||
if (customization) {
|
||||
const detected = await this.providedTaskConfigurations.getTaskToCustomize(token, customization, scope);
|
||||
if (detected) {
|
||||
return {
|
||||
...detected,
|
||||
...customization,
|
||||
type: detected.type
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** removes tasks configured in the given task config file */
|
||||
private removeTasks(scope: TaskConfigurationScope): void {
|
||||
const source = this.getKeyFromScope(scope);
|
||||
this.tasksMap.delete(source);
|
||||
this.taskCustomizationMap.delete(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes task customization objects found in the given task config file from the memory.
|
||||
* Please note: this function does not modify the task config file.
|
||||
*/
|
||||
private removeTaskCustomizations(scope: TaskConfigurationScope): void {
|
||||
const source = this.getKeyFromScope(scope);
|
||||
this.taskCustomizationMap.delete(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the task customizations by type from a given root folder in the workspace.
|
||||
* @param type the type of task customizations
|
||||
* @param rootFolder the root folder to find task customizations from. If `undefined`, this function returns an empty array.
|
||||
*/
|
||||
private getTaskCustomizations(type: string, scope: TaskConfigurationScope): TaskCustomization[] {
|
||||
const customizationInRootFolder = this.taskCustomizationMap.get(this.getKeyFromScope(scope));
|
||||
if (customizationInRootFolder) {
|
||||
return customizationInRootFolder.filter(c => c.type === type);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the customization object in `tasks.json` for the given task. Please note, this function
|
||||
* returns `undefined` if the given task is not a detected task, because configured tasks don't need
|
||||
* customization objects - users can modify its config directly in `tasks.json`.
|
||||
* @param taskConfig The task config, which could either be a configured task or a detected task.
|
||||
*/
|
||||
getCustomizationForTask(taskConfig: TaskConfiguration): TaskCustomization | undefined {
|
||||
if (!this.isDetectedTask(taskConfig)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const customizationByType = this.getTaskCustomizations(taskConfig.type, taskConfig._scope) || [];
|
||||
const hasCustomization = customizationByType.length > 0;
|
||||
if (hasCustomization) {
|
||||
const taskDefinition = this.taskDefinitionRegistry.getDefinition(taskConfig);
|
||||
if (taskDefinition) {
|
||||
const required = taskDefinition.properties.required || [];
|
||||
// Only support having one customization per task.
|
||||
return customizationByType.find(customization => required.every(property => customization[property] === taskConfig[property]));
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a change, to a config file we watch, is detected.
|
||||
*/
|
||||
protected async onDidTaskFileChange(fileChanges: TasksChange[]): Promise<void> {
|
||||
for (const change of fileChanges) {
|
||||
if (change.type === FileChangeType.DELETED) {
|
||||
this.removeTasks(change.scope);
|
||||
} else {
|
||||
// re-parse the config file
|
||||
await this.refreshTasks(change.scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the task configs from the task configuration manager, and updates the list of available tasks.
|
||||
*/
|
||||
protected async refreshTasks(scope: TaskConfigurationScope): Promise<void> {
|
||||
await this.readTasks(scope);
|
||||
|
||||
this.removeTasks(scope);
|
||||
this.removeTaskCustomizations(scope);
|
||||
|
||||
this.reorganizeTasks();
|
||||
}
|
||||
|
||||
/** parses a config file and extracts the tasks launch configurations */
|
||||
protected async readTasks(scope: TaskConfigurationScope): Promise<(TaskCustomization | TaskConfiguration)[] | undefined> {
|
||||
const rawConfigArray = this.taskConfigurationManager.getTasks(scope);
|
||||
const key = this.getKeyFromScope(scope);
|
||||
if (this.rawTaskConfigurations.has(key)) {
|
||||
this.rawTaskConfigurations.delete(key);
|
||||
}
|
||||
this.rawTaskConfigurations.set(key, rawConfigArray);
|
||||
return rawConfigArray;
|
||||
}
|
||||
|
||||
async openUserTasks(): Promise<void> {
|
||||
await this.taskConfigurationManager.openConfiguration(TaskScope.Global);
|
||||
}
|
||||
|
||||
/** Adds given task to a config file and opens the file to provide ability to edit task configuration. */
|
||||
async configure(token: number, task: TaskConfiguration): Promise<void> {
|
||||
const scope = task._scope;
|
||||
if (scope === TaskScope.Global) {
|
||||
return this.openUserTasks();
|
||||
}
|
||||
|
||||
const workspace = this.workspaceService.workspace;
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configuredAndCustomizedTasks = await this.getTasks(token);
|
||||
if (!configuredAndCustomizedTasks.some(t => this.taskDefinitionRegistry.compareTasks(t, task))) {
|
||||
await this.saveTask(scope, task);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.taskConfigurationManager.openConfiguration(scope);
|
||||
} catch (e) {
|
||||
console.error(`Error occurred while opening 'tasks.json' in ${this.taskSourceResolver.resolve(task)}.`, e);
|
||||
}
|
||||
}
|
||||
|
||||
private getTaskCustomizationTemplate(task: TaskConfiguration): TaskCustomization | undefined {
|
||||
const definition = this.getTaskDefinition(task);
|
||||
if (!definition) {
|
||||
console.error('Detected / Contributed tasks should have a task definition.');
|
||||
return;
|
||||
}
|
||||
const customization: TaskCustomization = { type: task.type, runOptions: task.runOptions };
|
||||
definition.properties.all.forEach(p => {
|
||||
if (task[p] !== undefined) {
|
||||
customization[p] = task[p];
|
||||
}
|
||||
});
|
||||
if ('problemMatcher' in task) {
|
||||
const problemMatcher: string[] = [];
|
||||
if (Array.isArray(task.problemMatcher)) {
|
||||
problemMatcher.push(...task.problemMatcher.map(t => {
|
||||
if (typeof t === 'string') {
|
||||
return t;
|
||||
} else {
|
||||
return t.name!;
|
||||
}
|
||||
}));
|
||||
} else if (typeof task.problemMatcher === 'string') {
|
||||
problemMatcher.push(task.problemMatcher);
|
||||
} else if (task.problemMatcher) {
|
||||
problemMatcher.push(task.problemMatcher.name!);
|
||||
}
|
||||
customization.problemMatcher = problemMatcher.map(asVariableName);
|
||||
}
|
||||
if (task.group) {
|
||||
customization.group = task.group;
|
||||
}
|
||||
|
||||
customization.label = task.label;
|
||||
|
||||
return { ...customization };
|
||||
}
|
||||
|
||||
/** Writes the task to a config file. Creates a config file if this one does not exist */
|
||||
saveTask(scope: TaskConfigurationScope, task: TaskConfiguration): Promise<boolean> {
|
||||
const { _source, $ident, ...preparedTask } = task;
|
||||
const customizedTaskTemplate = this.getTaskCustomizationTemplate(task) || preparedTask;
|
||||
return this.taskConfigurationManager.addTaskConfiguration(scope, customizedTaskTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called after a change in TaskDefinitionRegistry happens.
|
||||
* It checks all tasks that have been loaded, and re-organized them in `tasksMap` and `taskCustomizationMap`.
|
||||
*/
|
||||
protected reorganizeTasks(): void {
|
||||
const newTaskMap = new Map<string, Map<string, TaskConfiguration>>();
|
||||
const newTaskCustomizationMap = new Map<string, TaskCustomization[]>();
|
||||
const addCustomization = (rootFolder: string, customization: TaskCustomization) => {
|
||||
if (newTaskCustomizationMap.has(rootFolder)) {
|
||||
newTaskCustomizationMap.get(rootFolder)!.push(customization);
|
||||
} else {
|
||||
newTaskCustomizationMap.set(rootFolder, [customization]);
|
||||
}
|
||||
};
|
||||
const addConfiguredTask = (rootFolder: string, label: string, configuredTask: TaskCustomization) => {
|
||||
if (newTaskMap.has(rootFolder)) {
|
||||
newTaskMap.get(rootFolder)!.set(label, configuredTask as TaskConfiguration);
|
||||
} else {
|
||||
const newConfigMap = new Map();
|
||||
newConfigMap.set(label, configuredTask);
|
||||
newTaskMap.set(rootFolder, newConfigMap);
|
||||
}
|
||||
};
|
||||
|
||||
for (const [scopeKey, taskConfigs] of this.rawTaskConfigurations.entries()) {
|
||||
for (const taskConfig of taskConfigs) {
|
||||
const scope = this.getScopeFromKey(scopeKey);
|
||||
const isValid = this.isTaskConfigValid(taskConfig);
|
||||
if (!isValid) {
|
||||
continue;
|
||||
}
|
||||
const transformedTask = this.getTransformedRawTask(taskConfig, scope);
|
||||
if (this.isDetectedTask(transformedTask)) {
|
||||
addCustomization(scopeKey, transformedTask);
|
||||
} else {
|
||||
addConfiguredTask(scopeKey, transformedTask['label'] as string, transformedTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.taskCustomizationMap = newTaskCustomizationMap;
|
||||
this.tasksMap = newTaskMap;
|
||||
}
|
||||
|
||||
private getTransformedRawTask(rawTask: TaskCustomization | TaskConfiguration, scope: TaskConfigurationScope): TaskCustomization | TaskConfiguration {
|
||||
let taskConfig: TaskCustomization | TaskConfiguration;
|
||||
if (this.isDetectedTask(rawTask)) {
|
||||
const def = this.getTaskDefinition(rawTask);
|
||||
taskConfig = {
|
||||
...rawTask,
|
||||
_source: def!.source,
|
||||
_scope: scope
|
||||
};
|
||||
} else {
|
||||
taskConfig = {
|
||||
...rawTask,
|
||||
_source: scope,
|
||||
_scope: scope
|
||||
};
|
||||
}
|
||||
return {
|
||||
...taskConfig,
|
||||
presentation: TaskOutputPresentation.fromJson(rawTask)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the given task configuration is valid as per the task schema defined in Theia
|
||||
* or contributed by Theia extensions and plugins, `false` otherwise.
|
||||
*/
|
||||
private isTaskConfigValid(task: TaskCustomization): boolean {
|
||||
return this.taskSchemaUpdater.validate({ tasks: [task] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the task config in the `tasks.json`.
|
||||
* The task config, together with updates, will be written into the `tasks.json` if it is not found in the file.
|
||||
*
|
||||
* @param task task that the updates will be applied to
|
||||
* @param update the updates to be applied
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async updateTaskConfig(token: number, task: TaskConfiguration, update: { [name: string]: any }): Promise<void> {
|
||||
const scope = task._scope;
|
||||
const configuredAndCustomizedTasks = await this.getTasks(token);
|
||||
if (configuredAndCustomizedTasks.some(t => this.taskDefinitionRegistry.compareTasks(t, task))) { // task is already in `tasks.json`
|
||||
const jsonTasks = this.taskConfigurationManager.getTasks(scope);
|
||||
if (jsonTasks) {
|
||||
const ind = jsonTasks.findIndex((t: TaskCustomization | TaskConfiguration) => {
|
||||
if (t.type !== (task.type)) {
|
||||
return false;
|
||||
}
|
||||
const def = this.taskDefinitionRegistry.getDefinition(t);
|
||||
if (def) {
|
||||
return def.properties.all.every(p => t[p] === task[p]);
|
||||
}
|
||||
return t.label === task.label;
|
||||
});
|
||||
jsonTasks[ind] = {
|
||||
...jsonTasks[ind],
|
||||
...update
|
||||
};
|
||||
}
|
||||
this.taskConfigurationManager.setTaskConfigurations(scope, jsonTasks);
|
||||
} else { // task is not in `tasks.json`
|
||||
Object.keys(update).forEach(taskProperty => {
|
||||
task[taskProperty] = update[taskProperty];
|
||||
});
|
||||
this.saveTask(scope, task);
|
||||
}
|
||||
}
|
||||
|
||||
private getKeyFromScope(scope: TaskConfigurationScope): string {
|
||||
// Converting the enums to string will not yield a valid URI, so the keys will be distinct from any URI.
|
||||
return scope.toString();
|
||||
}
|
||||
|
||||
private getScopeFromKey(key: string): TaskConfigurationScope {
|
||||
if (TaskScope.Global.toString() === key) {
|
||||
return TaskScope.Global;
|
||||
} else if (TaskScope.Workspace.toString() === key) {
|
||||
return TaskScope.Workspace;
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/** checks if the config is a detected / contributed task */
|
||||
private isDetectedTask(task: TaskConfiguration | TaskCustomization): boolean {
|
||||
const taskDefinition = this.getTaskDefinition(task);
|
||||
// it is considered as a customization if the task definition registry finds a def for the task configuration
|
||||
return !!taskDefinition;
|
||||
}
|
||||
|
||||
private getTaskDefinition(task: TaskCustomization): TaskDefinition | undefined {
|
||||
return this.taskDefinitionRegistry.getDefinition(task);
|
||||
}
|
||||
}
|
||||
74
packages/task/src/browser/task-context-key-service.ts
Normal file
74
packages/task/src/browser/task-context-key-service.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
|
||||
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
|
||||
|
||||
@injectable()
|
||||
export class TaskContextKeyService {
|
||||
|
||||
@inject(ContextKeyService)
|
||||
protected readonly contextKeyService: ContextKeyService;
|
||||
|
||||
@inject(ApplicationServer)
|
||||
protected readonly applicationServer: ApplicationServer;
|
||||
|
||||
// The context keys are supposed to be aligned with VS Code. See also:
|
||||
// https://github.com/microsoft/vscode/blob/e6125a356ff6ebe7214b183ee1b5fb009a2b8d31/src/vs/workbench/contrib/tasks/common/taskService.ts#L20-L24
|
||||
protected customExecutionSupported: ContextKey<boolean>;
|
||||
protected shellExecutionSupported: ContextKey<boolean>;
|
||||
protected processExecutionSupported: ContextKey<boolean>;
|
||||
protected serverlessWebContext: ContextKey<boolean>;
|
||||
protected taskCommandsRegistered: ContextKey<boolean>;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.customExecutionSupported = this.contextKeyService.createKey('customExecutionSupported', true);
|
||||
this.shellExecutionSupported = this.contextKeyService.createKey('shellExecutionSupported', true);
|
||||
this.processExecutionSupported = this.contextKeyService.createKey('processExecutionSupported', true);
|
||||
this.serverlessWebContext = this.contextKeyService.createKey('serverlessWebContext', false);
|
||||
this.taskCommandsRegistered = this.contextKeyService.createKey('taskCommandsRegistered', true);
|
||||
this.applicationServer.getApplicationPlatform().then(platform => {
|
||||
if (platform === 'web') {
|
||||
this.setShellExecutionSupported(false);
|
||||
this.setProcessExecutionSupported(false);
|
||||
this.setServerlessWebContext(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCustomExecutionSupported(customExecutionSupported: boolean): void {
|
||||
this.customExecutionSupported.set(customExecutionSupported);
|
||||
}
|
||||
|
||||
setShellExecutionSupported(shellExecutionSupported: boolean): void {
|
||||
this.shellExecutionSupported.set(shellExecutionSupported);
|
||||
}
|
||||
|
||||
setProcessExecutionSupported(processExecutionSupported: boolean): void {
|
||||
this.processExecutionSupported.set(processExecutionSupported);
|
||||
}
|
||||
|
||||
setServerlessWebContext(serverlessWebContext: boolean): void {
|
||||
this.serverlessWebContext.set(serverlessWebContext);
|
||||
}
|
||||
|
||||
setTaskCommandsRegistered(taskCommandsRegistered: boolean): void {
|
||||
this.taskCommandsRegistered.set(taskCommandsRegistered);
|
||||
}
|
||||
|
||||
}
|
||||
266
packages/task/src/browser/task-contribution.ts
Normal file
266
packages/task/src/browser/task-contribution.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { TaskConfiguration } from '../common/task-protocol';
|
||||
import { WaitUntilEvent, Emitter } from '@theia/core/lib/common/event';
|
||||
|
||||
export const TaskContribution = Symbol('TaskContribution');
|
||||
|
||||
/**
|
||||
* A {@link TaskContribution} allows to contribute custom {@link TaskResolver}s and/or {@link TaskProvider}s.
|
||||
*
|
||||
* ### Example usage
|
||||
* ```typescript
|
||||
* @injectable()
|
||||
* export class ProcessTaskContribution implements TaskContribution {
|
||||
*
|
||||
* @inject(ProcessTaskResolver)
|
||||
* protected readonly processTaskResolver: ProcessTaskResolver;
|
||||
*
|
||||
* registerResolvers(resolvers: TaskResolverRegistry): void {
|
||||
* resolvers.register('process', this.processTaskResolver);
|
||||
* resolvers.register('shell', this.processTaskResolver);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface TaskContribution {
|
||||
/**
|
||||
* Register task resolvers using the given `TaskResolverRegistry`.
|
||||
* @param resolvers the task resolver registry.
|
||||
*/
|
||||
registerResolvers?(resolvers: TaskResolverRegistry): void;
|
||||
/**
|
||||
* Register task providers using the given `TaskProviderRegistry`.
|
||||
* @param resolvers the task provider registry.
|
||||
*/
|
||||
registerProviders?(providers: TaskProviderRegistry): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link TaskResolver} is used to preprocess/resolve a task before sending
|
||||
* it to the Task Server. For instance, the resolver can be used to add missing information to the configuration
|
||||
* (e.g default values for optional parameters).
|
||||
*/
|
||||
export interface TaskResolver {
|
||||
/**
|
||||
* Resolves a `TaskConfiguration` before sending it for execution to the `TaskServer` (Backend).
|
||||
* @param taskConfig the configuration that should be resolved.
|
||||
*
|
||||
* @returns a promise of the resolved `TaskConfiguration`.
|
||||
*/
|
||||
|
||||
resolveTask(taskConfig: TaskConfiguration): Promise<TaskConfiguration>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link TaskProvider} can be used to define the set of tasks that should
|
||||
* be provided to the system. i.e. that are available for the user to run.
|
||||
*/
|
||||
export interface TaskProvider {
|
||||
/**
|
||||
* Retrieves the task configurations which are provided programmatically to the system.
|
||||
*
|
||||
* @returns a promise of the provided tasks configurations.
|
||||
*/
|
||||
provideTasks(): Promise<TaskConfiguration[]>;
|
||||
}
|
||||
|
||||
export interface WillResolveTaskProvider extends WaitUntilEvent {
|
||||
taskType?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link TaskResolverRegistry} is the common component for registration and provision of
|
||||
* {@link TaskResolver}s. Theia will collect all {@link TaskContribution}s and invoke {@link TaskContribution#registerResolvers}
|
||||
* for each contribution.
|
||||
*/
|
||||
@injectable()
|
||||
export class TaskResolverRegistry {
|
||||
|
||||
protected readonly onWillProvideTaskResolverEmitter = new Emitter<WillResolveTaskProvider>();
|
||||
/**
|
||||
* Emit when the registry provides a registered resolver. i.e. when the {@link TaskResolverRegistry#getResolver}
|
||||
* function is called.
|
||||
*/
|
||||
readonly onWillProvideTaskResolver = this.onWillProvideTaskResolverEmitter.event;
|
||||
|
||||
protected taskResolvers: Map<string, TaskResolver> = new Map();
|
||||
protected executionResolvers: Map<string, TaskResolver> = new Map();
|
||||
|
||||
/**
|
||||
* Registers the given {@link TaskResolver} to resolve the `TaskConfiguration` of the specified type.
|
||||
* If there is already a `TaskResolver` registered for the specified type the registration will
|
||||
* be overwritten with the new value.
|
||||
*
|
||||
* @deprecated since 1.12.0 use `registerTaskResolver` instead.
|
||||
*
|
||||
* @param type the task configuration type for which the given resolver should be registered.
|
||||
* @param resolver the task resolver that should be registered.
|
||||
*
|
||||
* @returns a `Disposable` that can be invoked to unregister the given resolver
|
||||
*/
|
||||
register(type: string, resolver: TaskResolver): Disposable {
|
||||
return this.registerTaskResolver(type, resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given {@link TaskResolver} to resolve the `TaskConfiguration` of the specified type.
|
||||
* If there is already a `TaskResolver` registered for the specified type the registration will
|
||||
* be overwritten with the new value.
|
||||
*
|
||||
* @param type the task configuration type for which the given resolver should be registered.
|
||||
* @param resolver the task resolver that should be registered.
|
||||
*
|
||||
* @returns a `Disposable` that can be invoked to unregister the given resolver
|
||||
*/
|
||||
|
||||
registerTaskResolver(type: string, resolver: TaskResolver): Disposable {
|
||||
if (this.taskResolvers.has(type)) {
|
||||
console.warn(`Overriding task resolver for ${type}`);
|
||||
}
|
||||
this.taskResolvers.set(type, resolver);
|
||||
return {
|
||||
dispose: () => this.taskResolvers.delete(type)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link TaskResolver} registered for the given type task configuration type.
|
||||
*
|
||||
* @deprecated since 1.12.0 use `getTaskResolver()` instead.
|
||||
*
|
||||
* @param type the task configuration type
|
||||
*
|
||||
* @returns a promise of the registered `TaskResolver` or `undefined` if no resolver is registered for the given type.
|
||||
*/
|
||||
async getResolver(type: string): Promise<TaskResolver | undefined> {
|
||||
return this.getTaskResolver(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link TaskResolver} registered for the given type task configuration type.
|
||||
* @param type the task configuration type
|
||||
*
|
||||
* @returns a promise of the registered `TaskResolver` or `undefined` if no resolver is registered for the given type.
|
||||
*/
|
||||
async getTaskResolver(type: string): Promise<TaskResolver | undefined> {
|
||||
await WaitUntilEvent.fire(this.onWillProvideTaskResolverEmitter, { taskType: type });
|
||||
return this.taskResolvers.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given {@link TaskResolver} to resolve the `TaskConfiguration` for the
|
||||
* specified type of execution ('shell', 'process' or 'customExecution').
|
||||
* If there is already a `TaskResolver` registered for the specified type the registration will
|
||||
* be overwritten with the new value.
|
||||
*
|
||||
* @param type the task execution type for which the given resolver should be registered.
|
||||
* @param resolver the task resolver that should be registered.
|
||||
*
|
||||
* @returns a `Disposable` that can be invoked to unregister the given resolver
|
||||
*/
|
||||
registerExecutionResolver(type: string, resolver: TaskResolver): Disposable {
|
||||
if (this.executionResolvers.has(type)) {
|
||||
console.warn(`Overriding execution resolver for ${type}`);
|
||||
}
|
||||
this.executionResolvers.set(type, resolver);
|
||||
return {
|
||||
dispose: () => this.executionResolvers.delete(type)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link TaskResolver} registered for the given type of execution ('shell', 'process' or 'customExecution')..
|
||||
* @param type the task configuration type
|
||||
*
|
||||
* @returns a promise of the registered `TaskResolver` or `undefined` if no resolver is registered for the given type.
|
||||
*/
|
||||
getExecutionResolver(executionType: string): TaskResolver | undefined {
|
||||
return this.executionResolvers.get(executionType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link TaskProviderRegistry} is the common component for registration and provision of
|
||||
* {@link TaskProvider}s. Theia will collect all {@link TaskContribution}s and invoke {@link TaskContribution#registerProviders}
|
||||
* for each contribution.
|
||||
*/
|
||||
@injectable()
|
||||
export class TaskProviderRegistry {
|
||||
|
||||
protected readonly onWillProvideTaskProviderEmitter = new Emitter<WillResolveTaskProvider>();
|
||||
/**
|
||||
* Emit when the registry provides a registered task provider. i.e. when the {@link TaskProviderRegistry#getProvider}
|
||||
* function is called.
|
||||
*/
|
||||
readonly onWillProvideTaskProvider = this.onWillProvideTaskProviderEmitter.event;
|
||||
|
||||
protected providers: Map<string, TaskProvider>;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.providers = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given {@link TaskProvider} for task configurations of the specified type
|
||||
* @param type the task configuration type for which the given provider should be registered.
|
||||
* @param provider the `TaskProvider` that should be registered.
|
||||
*
|
||||
* @returns a `Disposable` that can be invoked to unregister the given resolver.
|
||||
*/
|
||||
register(type: string, provider: TaskProvider, handle?: number): Disposable {
|
||||
const key = handle === undefined ? type : `${type}::${handle}`;
|
||||
this.providers.set(key, provider);
|
||||
return {
|
||||
dispose: () => this.providers.delete(key)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates activation of a TaskProvider with the given type
|
||||
* @param type the task configuration type, '*' indicates, all providers.
|
||||
*/
|
||||
async activateProvider(type: string): Promise<void> {
|
||||
await WaitUntilEvent.fire(this.onWillProvideTaskProviderEmitter, { taskType: type });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link TaskProvider} registered for the given type task configuration type.
|
||||
* If there is already a `TaskProvider` registered for the specified type the registration will
|
||||
* be overwritten with the new value.
|
||||
* @param type the task configuration type.
|
||||
*
|
||||
* @returns a promise of the registered `TaskProvider`` or `undefined` if no provider is registered for the given type.
|
||||
*/
|
||||
async getProvider(type: string): Promise<TaskProvider | undefined> {
|
||||
await this.activateProvider(type);
|
||||
return this.providers.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all registered {@link TaskProvider}s.
|
||||
*
|
||||
* Use {@link activateProvider} to control registration of providers as needed.
|
||||
* @returns a promise of all registered {@link TaskProvider}s.
|
||||
*/
|
||||
async getProviders(): Promise<TaskProvider[]> {
|
||||
return [...this.providers.values()];
|
||||
}
|
||||
}
|
||||
203
packages/task/src/browser/task-definition-registry.spec.ts
Normal file
203
packages/task/src/browser/task-definition-registry.spec.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { PanelKind, RevealKind, TaskScope, TaskDefinition } from '../common';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
|
||||
describe('TaskDefinitionRegistry', () => {
|
||||
let registry: TaskDefinitionRegistry;
|
||||
const definitionContributionA: TaskDefinition = {
|
||||
taskType: 'extA',
|
||||
source: 'extA',
|
||||
properties: {
|
||||
required: ['extensionType'],
|
||||
all: ['extensionType', 'taskLabel'],
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['extensionType'],
|
||||
properties: {
|
||||
type: { const: 'extA' },
|
||||
extensionType: {},
|
||||
taskLabel: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const definitionContributionB: TaskDefinition = {
|
||||
taskType: 'extA',
|
||||
source: 'extA',
|
||||
properties: {
|
||||
required: ['extensionType', 'taskLabel', 'taskDetailedLabel'],
|
||||
all: ['extensionType', 'taskLabel', 'taskDetailedLabel'],
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['extensionType', 'taskLabel', 'taskDetailedLabel'],
|
||||
properties: {
|
||||
type: { const: 'extA' },
|
||||
extensionType: {},
|
||||
taskLabel: {},
|
||||
taskDetailedLabel: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const FAKE_TASK_META = {
|
||||
TYPE: 'foobar_type',
|
||||
SRC: 'foobar_src'
|
||||
};
|
||||
const defaultPresentation = {
|
||||
clear: false,
|
||||
echo: true,
|
||||
focus: false,
|
||||
panel: PanelKind.Shared,
|
||||
reveal: RevealKind.Always,
|
||||
showReuseMessage: true,
|
||||
};
|
||||
const fakeTaskDefinition: TaskDefinition = {
|
||||
taskType: FAKE_TASK_META.TYPE,
|
||||
source: FAKE_TASK_META.SRC,
|
||||
properties: {
|
||||
required: ['strArg'],
|
||||
all: ['strArg', 'arrArgs'],
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['strArg'],
|
||||
properties: {
|
||||
type: { const: FAKE_TASK_META.TYPE },
|
||||
strArg: {},
|
||||
arrArgs: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const configureFakeTask = (
|
||||
executionId = 'foobar',
|
||||
type = FAKE_TASK_META.TYPE,
|
||||
_source = FAKE_TASK_META.SRC,
|
||||
arrArgs: unknown[] = [],
|
||||
strArg = '',
|
||||
label = 'foobar',
|
||||
presentation = defaultPresentation,
|
||||
problemMatcher = undefined,
|
||||
taskType = 'customExecution',
|
||||
_scope = TaskScope.Workspace,
|
||||
) => ({
|
||||
executionId, arrArgs, strArg, label, presentation,
|
||||
problemMatcher, taskType, type, _scope, _source,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new TaskDefinitionRegistry();
|
||||
});
|
||||
|
||||
describe('register function', () => {
|
||||
it('should transform the task definition contribution and store it in memory', () => {
|
||||
registry.register(definitionContributionA);
|
||||
expect(registry['definitions'].get(definitionContributionA.taskType)).to.be.ok;
|
||||
expect(registry['definitions'].get(definitionContributionA.taskType)![0]).to.deep.equal(definitionContributionA);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinitions function', () => {
|
||||
it('should return all definitions associated with the given type', () => {
|
||||
registry.register(definitionContributionA);
|
||||
const defs1 = registry.getDefinitions(definitionContributionA.taskType);
|
||||
expect(defs1.length).to.eq(1);
|
||||
|
||||
registry.register(definitionContributionB);
|
||||
const defs2 = registry.getDefinitions(definitionContributionA.taskType);
|
||||
expect(defs2.length).to.eq(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefinition function', () => {
|
||||
it('should return undefined if the given task configuration does not match any registered definitions', () => {
|
||||
registry.register(definitionContributionA);
|
||||
registry.register(definitionContributionB);
|
||||
const defs = registry.getDefinition({
|
||||
type: definitionContributionA.taskType, label: 'grunt task', task: 'build'
|
||||
});
|
||||
expect(defs).to.be.not.ok;
|
||||
});
|
||||
|
||||
it('should return the best match if there is one or more registered definitions match the given task configuration', () => {
|
||||
registry.register(definitionContributionA);
|
||||
registry.register(definitionContributionB);
|
||||
const defs = registry.getDefinition({
|
||||
type: definitionContributionA.taskType, label: 'extension task', extensionType: 'extensionType', taskLabel: 'taskLabel'
|
||||
});
|
||||
expect(defs).to.be.ok;
|
||||
expect(defs!.taskType).to.be.eq(definitionContributionA.taskType);
|
||||
|
||||
const defs2 = registry.getDefinition({
|
||||
type: definitionContributionA.taskType, label: 'extension task', extensionType: 'extensionType', taskLabel: 'taskLabel', taskDetailedLabel: 'taskDetailedLabel'
|
||||
});
|
||||
expect(defs2).to.be.ok;
|
||||
expect(defs2!.taskType).to.be.eq(definitionContributionB.taskType);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareTasks function', () => {
|
||||
|
||||
beforeEach(() => registry.register(fakeTaskDefinition));
|
||||
|
||||
it('should return false if given 2 task configurations with different type', () => {
|
||||
const areSameTasks = registry.compareTasks(
|
||||
configureFakeTask('id_1', 'type_1'),
|
||||
configureFakeTask('id_2', 'type_2'),
|
||||
);
|
||||
expect(areSameTasks).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if given 2 same task configurations with empty arrays (different by reference) as custom property', () => {
|
||||
const areSameTasks = registry.compareTasks(
|
||||
configureFakeTask('id_1'),
|
||||
configureFakeTask('id_2'),
|
||||
);
|
||||
expect(areSameTasks).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true if given 2 same task configurations with deep properties (different by reference)', () => {
|
||||
const areSameTasks = registry.compareTasks(
|
||||
configureFakeTask('id_1', undefined, undefined, [1, '2', { '3': { a: true, b: 'string' } }]),
|
||||
configureFakeTask('id_2', undefined, undefined, [1, '2', { '3': { a: true, b: 'string' } }]),
|
||||
);
|
||||
expect(areSameTasks).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if given 2 task configurations with different deep properties', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const inputs: [any, any][] = [
|
||||
[
|
||||
configureFakeTask('id_1', undefined, undefined, [1, '2', { '3': { a: true, b: 'b' } }]),
|
||||
configureFakeTask('id_2', undefined, undefined, [1, '2', { '3': { a: true } }]),
|
||||
],
|
||||
[
|
||||
configureFakeTask('id_1', undefined, undefined, [1, '2']),
|
||||
configureFakeTask('id_2', undefined, undefined, [1, 2]),
|
||||
],
|
||||
[
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
configureFakeTask('id_1', undefined, undefined, [1, '2', { c: null }]),
|
||||
configureFakeTask('id_2', undefined, undefined, [1, '2', { c: undefined }]),
|
||||
],
|
||||
];
|
||||
const allAreFalse = inputs.map(args => registry.compareTasks(...args)).every(areSameTasks => areSameTasks === false);
|
||||
expect(allAreFalse).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
131
packages/task/src/browser/task-definition-registry.ts
Normal file
131
packages/task/src/browser/task-definition-registry.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { JSONExt } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { Event, Emitter } from '@theia/core/lib/common';
|
||||
import { TaskConfiguration, TaskDefinition, TaskCustomization } from '../common';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
|
||||
@injectable()
|
||||
export class TaskDefinitionRegistry {
|
||||
|
||||
// task type - array of task definitions
|
||||
private definitions: Map<string, TaskDefinition[]> = new Map();
|
||||
|
||||
protected readonly onDidRegisterTaskDefinitionEmitter = new Emitter<void>();
|
||||
get onDidRegisterTaskDefinition(): Event<void> {
|
||||
return this.onDidRegisterTaskDefinitionEmitter.event;
|
||||
}
|
||||
|
||||
protected readonly onDidUnregisterTaskDefinitionEmitter = new Emitter<void>();
|
||||
get onDidUnregisterTaskDefinition(): Event<void> {
|
||||
return this.onDidUnregisterTaskDefinitionEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all task definitions that are registered
|
||||
* @return the task definitions that are registered
|
||||
*/
|
||||
getAll(): TaskDefinition[] {
|
||||
const all: TaskDefinition[] = [];
|
||||
for (const definitions of this.definitions.values()) {
|
||||
all.push(...definitions);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the task definition(s) from the registry with the given `taskType`.
|
||||
*
|
||||
* @param taskType the type of the task
|
||||
* @return an array of the task definitions. If no task definitions are found, an empty array is returned.
|
||||
*/
|
||||
getDefinitions(taskType: string): TaskDefinition[] {
|
||||
return this.definitions.get(taskType) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the task definition from the registry for the task configuration.
|
||||
* The task configuration is considered as a "match" to the task definition if it has all the `required` properties.
|
||||
* In case that more than one task definition is found, return the one that has the biggest number of matched properties.
|
||||
*
|
||||
* @param taskConfiguration the task configuration
|
||||
* @return the task definition for the task configuration. If the task definition is not found, `undefined` is returned.
|
||||
*/
|
||||
getDefinition(taskConfiguration: TaskConfiguration | TaskCustomization): TaskDefinition | undefined {
|
||||
const definitions = this.getDefinitions(taskConfiguration.type);
|
||||
let matchedDefinition: TaskDefinition | undefined;
|
||||
let highest = -1;
|
||||
for (const def of definitions) {
|
||||
const required = def.properties.required || [];
|
||||
if (!required.every(requiredProp => taskConfiguration[requiredProp] !== undefined)) {
|
||||
continue;
|
||||
}
|
||||
let score = required.length; // number of required properties
|
||||
const requiredProps = new Set(required);
|
||||
// number of optional properties
|
||||
score += def.properties.all.filter(p => !requiredProps.has(p) && taskConfiguration[p] !== undefined).length;
|
||||
if (score > highest) {
|
||||
highest = score;
|
||||
matchedDefinition = def;
|
||||
}
|
||||
}
|
||||
return matchedDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task definition to the registry.
|
||||
*
|
||||
* @param definition the task definition to be added.
|
||||
*/
|
||||
register(definition: TaskDefinition): Disposable {
|
||||
const taskType = definition.taskType;
|
||||
const definitions = this.definitions.get(taskType) || [];
|
||||
definitions.push(definition);
|
||||
this.definitions.set(taskType, definitions);
|
||||
this.onDidRegisterTaskDefinitionEmitter.fire(undefined);
|
||||
return Disposable.create(() => {
|
||||
const index = definitions.indexOf(definition);
|
||||
if (index !== -1) {
|
||||
definitions.splice(index, 1);
|
||||
}
|
||||
this.onDidUnregisterTaskDefinitionEmitter.fire(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
compareTasks(one: TaskConfiguration | TaskCustomization, other: TaskConfiguration | TaskCustomization): boolean {
|
||||
const oneType = one.type;
|
||||
const otherType = other.type;
|
||||
if (oneType !== otherType) {
|
||||
return false;
|
||||
}
|
||||
if (one['taskType'] !== other['taskType']) {
|
||||
return false;
|
||||
}
|
||||
const def = this.getDefinition(one);
|
||||
if (def) {
|
||||
// scope is either a string or an enum value. Anyway...they must exactly match
|
||||
// "_scope" may hold the Uri to the associated workspace whereas
|
||||
// "scope" reflects the original TaskConfigurationScope as provided by plugins,
|
||||
// Matching "_scope" or "scope" are both accepted in order to correlate provided task
|
||||
// configurations (e.g. TaskScope.Workspace) against already configured tasks.
|
||||
return def.properties.all.every(p => p === 'type' || JSONExt.deepEqual(one[p], other[p]))
|
||||
&& (one._scope === other._scope || one.scope === other.scope);
|
||||
}
|
||||
return one.label === other.label && one._source === other._source;
|
||||
}
|
||||
}
|
||||
402
packages/task/src/browser/task-frontend-contribution.ts
Normal file
402
packages/task/src/browser/task-frontend-contribution.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { ILogger, ContributionProvider, CommandContribution, Command, CommandRegistry, MenuContribution, MenuModelRegistry, nls } from '@theia/core/lib/common';
|
||||
import { QuickOpenTask, TaskTerminateQuickOpen, TaskRunningQuickOpen, TaskRestartRunningQuickOpen } from './quick-open-task';
|
||||
import {
|
||||
FrontendApplication, FrontendApplicationContribution, QuickAccessContribution,
|
||||
KeybindingRegistry, KeybindingContribution, StorageService, StatusBar, StatusBarAlignment, CommonMenus
|
||||
} from '@theia/core/lib/browser';
|
||||
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
|
||||
import { TaskContribution, TaskResolverRegistry, TaskProviderRegistry } from './task-contribution';
|
||||
import { TaskService } from './task-service';
|
||||
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
import { TaskSchemaUpdater } from './task-schema-updater';
|
||||
import { TaskConfiguration, TaskWatcher } from '../common';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
|
||||
export namespace TaskCommands {
|
||||
const TASK_CATEGORY = 'Task';
|
||||
const TASK_CATEGORY_KEY = nls.getDefaultKey(TASK_CATEGORY);
|
||||
export const TASK_RUN = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:run',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Run Task...'
|
||||
});
|
||||
|
||||
export const TASK_RUN_BUILD = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:run:build',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Run Build Task'
|
||||
});
|
||||
|
||||
export const TASK_RUN_TEST = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:run:test',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Run Test Task'
|
||||
});
|
||||
|
||||
export const WORKBENCH_RUN_TASK = Command.toLocalizedCommand({
|
||||
id: 'workbench.action.tasks.runTask',
|
||||
category: TASK_CATEGORY
|
||||
}, '', TASK_CATEGORY_KEY);
|
||||
|
||||
export const TASK_RUN_LAST = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:run:last',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Rerun Last Task'
|
||||
});
|
||||
|
||||
export const TASK_ATTACH = Command.toLocalizedCommand({
|
||||
id: 'task:attach',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Attach Task...'
|
||||
}, 'theia/task/attachTask', TASK_CATEGORY_KEY);
|
||||
|
||||
export const TASK_RUN_TEXT = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:run:text',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Run Selected Text'
|
||||
});
|
||||
|
||||
export const TASK_CONFIGURE = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:configure',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Configure Tasks...'
|
||||
});
|
||||
|
||||
export const TASK_OPEN_USER = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:open_user',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Open User Tasks'
|
||||
});
|
||||
|
||||
export const TASK_CLEAR_HISTORY = Command.toLocalizedCommand({
|
||||
id: 'task:clear-history',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Clear History'
|
||||
}, 'theia/task/clearHistory', TASK_CATEGORY_KEY);
|
||||
|
||||
export const TASK_SHOW_RUNNING = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:show-running',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Show Running Tasks'
|
||||
});
|
||||
|
||||
export const TASK_TERMINATE = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:terminate',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Terminate Task'
|
||||
});
|
||||
|
||||
export const TASK_RESTART_RUNNING = Command.toDefaultLocalizedCommand({
|
||||
id: 'task:restart-running',
|
||||
category: TASK_CATEGORY,
|
||||
label: 'Restart Running Task...'
|
||||
});
|
||||
}
|
||||
|
||||
const TASKS_STORAGE_KEY = 'tasks';
|
||||
|
||||
@injectable()
|
||||
export class TaskFrontendContribution implements CommandContribution, MenuContribution, KeybindingContribution, FrontendApplicationContribution, QuickAccessContribution {
|
||||
@inject(QuickOpenTask)
|
||||
protected readonly quickOpenTask: QuickOpenTask;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(FrontendApplication)
|
||||
protected readonly app: FrontendApplication;
|
||||
|
||||
@inject(ILogger) @named('task')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(WidgetManager)
|
||||
protected readonly widgetManager: WidgetManager;
|
||||
|
||||
@inject(ContributionProvider) @named(TaskContribution)
|
||||
protected readonly contributionProvider: ContributionProvider<TaskContribution>;
|
||||
|
||||
@inject(TaskProviderRegistry)
|
||||
protected readonly taskProviderRegistry: TaskProviderRegistry;
|
||||
|
||||
@inject(TaskResolverRegistry)
|
||||
protected readonly taskResolverRegistry: TaskResolverRegistry;
|
||||
|
||||
@inject(TaskService)
|
||||
protected readonly taskService: TaskService;
|
||||
|
||||
@inject(TaskSchemaUpdater)
|
||||
protected readonly schemaUpdater: TaskSchemaUpdater;
|
||||
|
||||
@inject(StorageService)
|
||||
protected readonly storageService: StorageService;
|
||||
|
||||
@inject(TaskRunningQuickOpen)
|
||||
protected readonly taskRunningQuickOpen: TaskRunningQuickOpen;
|
||||
|
||||
@inject(TaskTerminateQuickOpen)
|
||||
protected readonly taskTerminateQuickOpen: TaskTerminateQuickOpen;
|
||||
|
||||
@inject(TaskRestartRunningQuickOpen)
|
||||
protected readonly taskRestartRunningQuickOpen: TaskRestartRunningQuickOpen;
|
||||
|
||||
@inject(TaskWatcher)
|
||||
protected readonly taskWatcher: TaskWatcher;
|
||||
|
||||
@inject(StatusBar)
|
||||
protected readonly statusBar: StatusBar;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.taskWatcher.onTaskCreated(() => this.updateRunningTasksItem());
|
||||
this.taskWatcher.onTaskExit(() => this.updateRunningTasksItem());
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.contributionProvider.getContributions().forEach(contrib => {
|
||||
if (contrib.registerResolvers) {
|
||||
contrib.registerResolvers(this.taskResolverRegistry);
|
||||
}
|
||||
if (contrib.registerProviders) {
|
||||
contrib.registerProviders(this.taskProviderRegistry);
|
||||
}
|
||||
});
|
||||
this.schemaUpdater.update();
|
||||
|
||||
this.storageService.getData<{ recent: TaskConfiguration[] }>(TASKS_STORAGE_KEY, { recent: [] })
|
||||
.then(tasks => this.taskService.recentTasks = tasks.recent);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
const recent = this.taskService.recentTasks;
|
||||
this.storageService.setData<{ recent: TaskConfiguration[] }>(TASKS_STORAGE_KEY, { recent });
|
||||
}
|
||||
|
||||
/**
|
||||
* Contribute a status-bar item to trigger
|
||||
* the `Show Running Tasks` command.
|
||||
*/
|
||||
protected async updateRunningTasksItem(): Promise<void> {
|
||||
const id = 'show-running-tasks';
|
||||
const items = await this.taskService.getRunningTasks();
|
||||
if (!!items.length) {
|
||||
this.statusBar.setElement(id, {
|
||||
text: `$(tools) ${items.length}`,
|
||||
tooltip: TaskCommands.TASK_SHOW_RUNNING.label,
|
||||
alignment: StatusBarAlignment.LEFT,
|
||||
priority: 2,
|
||||
command: TaskCommands.TASK_SHOW_RUNNING.id,
|
||||
});
|
||||
} else {
|
||||
this.statusBar.removeElement(id);
|
||||
}
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(
|
||||
TaskCommands.WORKBENCH_RUN_TASK,
|
||||
{
|
||||
isEnabled: () => true,
|
||||
execute: async (label: string) => {
|
||||
const didExecute = await this.taskService.runTaskByLabel(this.taskService.startUserAction(), label);
|
||||
if (!didExecute) {
|
||||
this.quickOpenTask.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_RUN,
|
||||
{
|
||||
isEnabled: () => true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: (...args: any[]) => {
|
||||
const [source, label, scope] = args;
|
||||
if (source && label) {
|
||||
return this.taskService.run(this.taskService.startUserAction(), source, label, scope);
|
||||
}
|
||||
return this.quickOpenTask.open();
|
||||
}
|
||||
}
|
||||
);
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_RUN_BUILD,
|
||||
{
|
||||
isEnabled: () => this.workspaceService.opened,
|
||||
execute: () =>
|
||||
this.quickOpenTask.runBuildOrTestTask('build')
|
||||
}
|
||||
);
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_RUN_TEST,
|
||||
{
|
||||
isEnabled: () => this.workspaceService.opened,
|
||||
execute: () =>
|
||||
this.quickOpenTask.runBuildOrTestTask('test')
|
||||
}
|
||||
);
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_ATTACH,
|
||||
{
|
||||
isEnabled: () => true,
|
||||
execute: () => this.quickOpenTask.attach()
|
||||
}
|
||||
);
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_RUN_LAST,
|
||||
{
|
||||
execute: async () => {
|
||||
if (!await this.taskService.runLastTask(this.taskService.startUserAction())) {
|
||||
await this.quickOpenTask.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_RUN_TEXT,
|
||||
{
|
||||
isVisible: () => !!this.editorManager.currentEditor,
|
||||
isEnabled: () => !!this.editorManager.currentEditor,
|
||||
execute: () => this.taskService.runSelectedText()
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_CONFIGURE,
|
||||
{
|
||||
execute: () => this.quickOpenTask.configure()
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_OPEN_USER,
|
||||
{
|
||||
execute: () => {
|
||||
this.taskService.openUserTasks();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_CLEAR_HISTORY,
|
||||
{
|
||||
execute: () => this.taskService.clearRecentTasks()
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_SHOW_RUNNING,
|
||||
{
|
||||
execute: () => this.taskRunningQuickOpen.open()
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_TERMINATE,
|
||||
{
|
||||
execute: () => this.taskTerminateQuickOpen.open()
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerCommand(
|
||||
TaskCommands.TASK_RESTART_RUNNING,
|
||||
{
|
||||
execute: () => this.taskRestartRunningQuickOpen.open()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
|
||||
commandId: TaskCommands.TASK_RUN.id,
|
||||
order: '0'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
|
||||
commandId: TaskCommands.TASK_RUN_BUILD.id,
|
||||
order: '1'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
|
||||
commandId: TaskCommands.TASK_RUN_TEST.id,
|
||||
order: '2'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
|
||||
commandId: TaskCommands.TASK_RUN_LAST.id,
|
||||
order: '3'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
|
||||
commandId: TaskCommands.TASK_ATTACH.id,
|
||||
order: '4'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
|
||||
commandId: TaskCommands.TASK_RUN_TEXT.id,
|
||||
order: '5'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS_INFO, {
|
||||
commandId: TaskCommands.TASK_SHOW_RUNNING.id,
|
||||
label: TaskCommands.TASK_SHOW_RUNNING.label + '...',
|
||||
order: '0'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS_INFO, {
|
||||
commandId: TaskCommands.TASK_RESTART_RUNNING.id,
|
||||
label: TaskCommands.TASK_RESTART_RUNNING.label,
|
||||
order: '1'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS_INFO, {
|
||||
commandId: TaskCommands.TASK_TERMINATE.id,
|
||||
label: TaskCommands.TASK_TERMINATE.label + '...',
|
||||
order: '2'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS_CONFIG, {
|
||||
commandId: TaskCommands.TASK_CONFIGURE.id,
|
||||
order: '0'
|
||||
});
|
||||
|
||||
menus.registerMenuAction(CommonMenus.MANAGE_SETTINGS, {
|
||||
commandId: TaskCommands.TASK_OPEN_USER.id,
|
||||
label: nls.localizeByDefault('Tasks'),
|
||||
order: 'a40'
|
||||
});
|
||||
}
|
||||
|
||||
registerQuickAccessProvider(): void {
|
||||
this.quickOpenTask.registerQuickAccessProvider();
|
||||
}
|
||||
|
||||
registerKeybindings(keybindings: KeybindingRegistry): void {
|
||||
keybindings.registerKeybinding({
|
||||
command: TaskCommands.TASK_RUN_LAST.id,
|
||||
keybinding: 'ctrlcmd+shift+k',
|
||||
when: '!textInputFocus || editorReadonly'
|
||||
});
|
||||
}
|
||||
}
|
||||
88
packages/task/src/browser/task-frontend-module.ts
Normal file
88
packages/task/src/browser/task-frontend-module.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser';
|
||||
import { CommandContribution, MenuContribution, bindContributionProvider } from '@theia/core/lib/common';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging';
|
||||
import { QuickOpenTask, TaskTerminateQuickOpen, TaskRestartRunningQuickOpen, TaskRunningQuickOpen } from './quick-open-task';
|
||||
import { TaskContribution, TaskProviderRegistry, TaskResolverRegistry } from './task-contribution';
|
||||
import { TaskService } from './task-service';
|
||||
import { TaskConfigurations } from './task-configurations';
|
||||
import { ProvidedTaskConfigurations } from './provided-task-configurations';
|
||||
import { TaskFrontendContribution } from './task-frontend-contribution';
|
||||
import { createCommonBindings } from '../common/task-common-module';
|
||||
import { TaskServer, taskPath } from '../common/task-protocol';
|
||||
import { TaskWatcher } from '../common/task-watcher';
|
||||
import { bindProcessTaskModule } from './process/process-task-frontend-module';
|
||||
import { TaskSchemaUpdater } from './task-schema-updater';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import { ProblemMatcherRegistry } from './task-problem-matcher-registry';
|
||||
import { ProblemPatternRegistry } from './task-problem-pattern-registry';
|
||||
import { TaskConfigurationManager } from './task-configuration-manager';
|
||||
import { bindTaskPreferences } from '../common/task-preferences';
|
||||
import '../../src/browser/style/index.css';
|
||||
import './tasks-monaco-contribution';
|
||||
import { TaskNameResolver } from './task-name-resolver';
|
||||
import { TaskSourceResolver } from './task-source-resolver';
|
||||
import { TaskTemplateSelector } from './task-templates';
|
||||
import { TaskTerminalWidgetManager } from './task-terminal-widget-manager';
|
||||
import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
|
||||
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
|
||||
import { TaskContextKeyService } from './task-context-key-service';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(TaskFrontendContribution).toSelf().inSingletonScope();
|
||||
bind(TaskService).toSelf().inSingletonScope();
|
||||
|
||||
for (const identifier of [FrontendApplicationContribution, CommandContribution, KeybindingContribution, MenuContribution, QuickAccessContribution]) {
|
||||
bind(identifier).toService(TaskFrontendContribution);
|
||||
}
|
||||
|
||||
bind(QuickOpenTask).toSelf().inSingletonScope();
|
||||
bind(TaskRunningQuickOpen).toSelf().inSingletonScope();
|
||||
bind(TaskTerminateQuickOpen).toSelf().inSingletonScope();
|
||||
bind(TaskRestartRunningQuickOpen).toSelf().inSingletonScope();
|
||||
bind(TaskConfigurations).toSelf().inSingletonScope();
|
||||
bind(ProvidedTaskConfigurations).toSelf().inSingletonScope();
|
||||
bind(TaskConfigurationManager).toSelf().inSingletonScope();
|
||||
|
||||
bind(TaskServer).toDynamicValue(ctx => {
|
||||
const connection = ctx.container.get(WebSocketConnectionProvider);
|
||||
const taskWatcher = ctx.container.get(TaskWatcher);
|
||||
return connection.createProxy<TaskServer>(taskPath, taskWatcher.getTaskClient());
|
||||
}).inSingletonScope();
|
||||
|
||||
bind(TaskDefinitionRegistry).toSelf().inSingletonScope();
|
||||
bind(ProblemMatcherRegistry).toSelf().inSingletonScope();
|
||||
bind(ProblemPatternRegistry).toSelf().inSingletonScope();
|
||||
|
||||
createCommonBindings(bind);
|
||||
|
||||
bind(TaskProviderRegistry).toSelf().inSingletonScope();
|
||||
bind(TaskResolverRegistry).toSelf().inSingletonScope();
|
||||
bindContributionProvider(bind, TaskContribution);
|
||||
bind(TaskSchemaUpdater).toSelf().inSingletonScope();
|
||||
bind(JsonSchemaContribution).toService(TaskSchemaUpdater);
|
||||
bind(TaskNameResolver).toSelf().inSingletonScope();
|
||||
bind(TaskSourceResolver).toSelf().inSingletonScope();
|
||||
bind(TaskTemplateSelector).toSelf().inSingletonScope();
|
||||
bind(TaskTerminalWidgetManager).toSelf().inSingletonScope();
|
||||
bind(TaskContextKeyService).toSelf().inSingletonScope();
|
||||
|
||||
bindProcessTaskModule(bind);
|
||||
bindTaskPreferences(bind);
|
||||
});
|
||||
55
packages/task/src/browser/task-name-resolver.ts
Normal file
55
packages/task/src/browser/task-name-resolver.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { TaskConfiguration } from '../common';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import { TaskConfigurations } from './task-configurations';
|
||||
|
||||
@injectable()
|
||||
export class TaskNameResolver {
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(TaskConfigurations)
|
||||
protected readonly taskConfigurations: TaskConfigurations;
|
||||
|
||||
/**
|
||||
* Returns task name to display.
|
||||
* It is aligned with VS Code.
|
||||
*/
|
||||
resolve(task: TaskConfiguration): string {
|
||||
if (this.isDetectedTask(task)) {
|
||||
const scope = task._scope;
|
||||
const rawConfigs = this.taskConfigurations.getRawTaskConfigurations(scope);
|
||||
const jsonConfig = rawConfigs.find(rawConfig => this.taskDefinitionRegistry.compareTasks({
|
||||
...rawConfig, _scope: scope
|
||||
}, task));
|
||||
// detected task that has a `label` defined in `tasks.json`
|
||||
if (jsonConfig && jsonConfig.label) {
|
||||
return jsonConfig.label;
|
||||
}
|
||||
return `${task.source || task._source}: ${task.label}`;
|
||||
}
|
||||
|
||||
// it is a hack, when task is customized but extension is absent
|
||||
return task.label || `${task.type}: ${task.task}`;
|
||||
}
|
||||
|
||||
private isDetectedTask(task: TaskConfiguration): boolean {
|
||||
return !!this.taskDefinitionRegistry.getDefinition(task);
|
||||
}
|
||||
}
|
||||
37
packages/task/src/browser/task-node.ts
Normal file
37
packages/task/src/browser/task-node.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 SAP SE or an SAP affiliate company 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { TaskConfiguration } from '../common';
|
||||
|
||||
export class TaskNode {
|
||||
|
||||
taskId: TaskConfiguration;
|
||||
childTasks: TaskNode[];
|
||||
parentsID: TaskConfiguration[];
|
||||
|
||||
constructor(taskId: TaskConfiguration, childTasks: TaskNode[], parentsID: TaskConfiguration[]) {
|
||||
this.taskId = taskId;
|
||||
this.childTasks = childTasks;
|
||||
this.parentsID = parentsID;
|
||||
}
|
||||
|
||||
addChildDependency(node: TaskNode): void {
|
||||
this.childTasks.push(node);
|
||||
}
|
||||
|
||||
addParentDependency(parentId: TaskConfiguration): void {
|
||||
this.parentsID.push(parentId);
|
||||
}
|
||||
}
|
||||
308
packages/task/src/browser/task-problem-matcher-registry.ts
Normal file
308
packages/task/src/browser/task-problem-matcher-registry.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Event, Emitter, nls } from '@theia/core/lib/common';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
ApplyToKind, FileLocationKind, NamedProblemMatcher,
|
||||
ProblemPattern, ProblemMatcher, ProblemMatcherContribution, WatchingMatcher,
|
||||
fromVariableName
|
||||
} from '../common';
|
||||
import { ProblemPatternRegistry } from './task-problem-pattern-registry';
|
||||
import { Severity } from '@theia/core/lib/common/severity';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
|
||||
@injectable()
|
||||
export class ProblemMatcherRegistry {
|
||||
|
||||
private readonly matchers = new Map<string, NamedProblemMatcher>();
|
||||
private readyPromise = new Deferred<void>();
|
||||
|
||||
@inject(ProblemPatternRegistry)
|
||||
protected readonly problemPatternRegistry: ProblemPatternRegistry;
|
||||
|
||||
protected readonly onDidChangeProblemMatcherEmitter = new Emitter<void>();
|
||||
get onDidChangeProblemMatcher(): Event<void> {
|
||||
return this.onDidChangeProblemMatcherEmitter.event;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.problemPatternRegistry.onReady().then(() => {
|
||||
this.fillDefaults();
|
||||
this.readyPromise.resolve();
|
||||
this.onDidChangeProblemMatcherEmitter.fire(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
onReady(): Promise<void> {
|
||||
return this.readyPromise.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a problem matcher to the registry.
|
||||
*
|
||||
* @param definition the problem matcher to be added.
|
||||
*/
|
||||
register(matcher: ProblemMatcherContribution): Disposable {
|
||||
if (!matcher.name) {
|
||||
console.error('Only named Problem Matchers can be registered.');
|
||||
return Disposable.NULL;
|
||||
}
|
||||
const toDispose = new DisposableCollection(Disposable.create(() => {
|
||||
/* mark as not disposed */
|
||||
this.onDidChangeProblemMatcherEmitter.fire(undefined);
|
||||
}));
|
||||
this.doRegister(matcher, toDispose).then(() => this.onDidChangeProblemMatcherEmitter.fire(undefined));
|
||||
return toDispose;
|
||||
}
|
||||
|
||||
protected async doRegister(matcher: ProblemMatcherContribution, toDispose: DisposableCollection): Promise<void> {
|
||||
const problemMatcher = await this.getProblemMatcherFromContribution(matcher);
|
||||
if (toDispose.disposed) {
|
||||
return;
|
||||
}
|
||||
toDispose.push(this.add(problemMatcher as NamedProblemMatcher));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the problem matcher from the registry by its name.
|
||||
*
|
||||
* @param name the name of the problem matcher
|
||||
* @return the problem matcher. If the task definition is not found, `undefined` is returned.
|
||||
*/
|
||||
get(name: string): NamedProblemMatcher | undefined {
|
||||
return this.matchers.get(fromVariableName(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered problem matchers in the registry.
|
||||
*/
|
||||
getAll(): NamedProblemMatcher[] {
|
||||
const all: NamedProblemMatcher[] = [];
|
||||
for (const matcherName of this.matchers.keys()) {
|
||||
all.push(this.get(matcherName)!);
|
||||
}
|
||||
all.sort((one, other) => one.name.localeCompare(other.name));
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the `ProblemMatcherContribution` to a `ProblemMatcher`
|
||||
*
|
||||
* @return the problem matcher
|
||||
*/
|
||||
async getProblemMatcherFromContribution(matcher: ProblemMatcherContribution): Promise<ProblemMatcher> {
|
||||
let baseMatcher: NamedProblemMatcher | undefined;
|
||||
if (matcher.base) {
|
||||
baseMatcher = this.get(matcher.base);
|
||||
}
|
||||
|
||||
let fileLocation: FileLocationKind | undefined;
|
||||
let filePrefix: string | undefined;
|
||||
if (matcher.fileLocation === undefined) {
|
||||
fileLocation = baseMatcher ? baseMatcher.fileLocation : FileLocationKind.Relative;
|
||||
filePrefix = baseMatcher ? baseMatcher.filePrefix : '${workspaceFolder}';
|
||||
} else {
|
||||
const locationAndPrefix = this.getFileLocationKindAndPrefix(matcher);
|
||||
fileLocation = locationAndPrefix.fileLocation;
|
||||
filePrefix = locationAndPrefix.filePrefix;
|
||||
}
|
||||
|
||||
const patterns: ProblemPattern[] = [];
|
||||
if (matcher.pattern) {
|
||||
if (typeof matcher.pattern === 'string') {
|
||||
await this.problemPatternRegistry.onReady();
|
||||
const registeredPattern = this.problemPatternRegistry.get(fromVariableName(matcher.pattern));
|
||||
if (Array.isArray(registeredPattern)) {
|
||||
patterns.push(...registeredPattern);
|
||||
} else if (!!registeredPattern) {
|
||||
patterns.push(registeredPattern);
|
||||
}
|
||||
} else if (Array.isArray(matcher.pattern)) {
|
||||
patterns.push(...matcher.pattern.map(p => ProblemPattern.fromProblemPatternContribution(p)));
|
||||
} else {
|
||||
patterns.push(ProblemPattern.fromProblemPatternContribution(matcher.pattern));
|
||||
}
|
||||
} else if (baseMatcher) {
|
||||
if (Array.isArray(baseMatcher.pattern)) {
|
||||
patterns.push(...baseMatcher.pattern);
|
||||
} else {
|
||||
patterns.push(baseMatcher.pattern);
|
||||
}
|
||||
}
|
||||
|
||||
let deprecated: boolean | undefined = matcher.deprecated;
|
||||
if (deprecated === undefined && baseMatcher) {
|
||||
deprecated = baseMatcher.deprecated;
|
||||
}
|
||||
|
||||
let applyTo: ApplyToKind | undefined;
|
||||
if (matcher.applyTo === undefined) {
|
||||
applyTo = baseMatcher ? baseMatcher.applyTo : ApplyToKind.allDocuments;
|
||||
} else {
|
||||
applyTo = ApplyToKind.fromString(matcher.applyTo) || ApplyToKind.allDocuments;
|
||||
}
|
||||
|
||||
let severity: Severity = Severity.fromValue(matcher.severity);
|
||||
if (matcher.severity === undefined && baseMatcher && baseMatcher.severity !== undefined) {
|
||||
severity = baseMatcher.severity;
|
||||
}
|
||||
let watching: WatchingMatcher | undefined = WatchingMatcher.fromWatchingMatcherContribution(matcher.background || matcher.watching);
|
||||
if (watching === undefined && baseMatcher) {
|
||||
watching = baseMatcher.watching;
|
||||
}
|
||||
const problemMatcher = {
|
||||
name: matcher.name || (baseMatcher ? baseMatcher.name : undefined),
|
||||
label: matcher.label || baseMatcher?.label || '',
|
||||
deprecated,
|
||||
owner: matcher.owner || (baseMatcher ? baseMatcher.owner : ''),
|
||||
source: matcher.source || (baseMatcher ? baseMatcher.source : undefined),
|
||||
applyTo,
|
||||
fileLocation,
|
||||
filePrefix,
|
||||
pattern: patterns,
|
||||
severity,
|
||||
watching
|
||||
};
|
||||
return problemMatcher;
|
||||
}
|
||||
|
||||
private add(matcher: NamedProblemMatcher): Disposable {
|
||||
this.matchers.set(matcher.name, matcher);
|
||||
return Disposable.create(() => this.matchers.delete(matcher.name));
|
||||
}
|
||||
|
||||
private getFileLocationKindAndPrefix(matcher: ProblemMatcherContribution): { fileLocation: FileLocationKind, filePrefix: string } {
|
||||
let fileLocation = FileLocationKind.Relative;
|
||||
let filePrefix = '${workspaceFolder}';
|
||||
if (matcher.fileLocation !== undefined) {
|
||||
if (Array.isArray(matcher.fileLocation)) {
|
||||
if (matcher.fileLocation.length > 0) {
|
||||
const locationKind = FileLocationKind.fromString(matcher.fileLocation[0]);
|
||||
if (matcher.fileLocation.length === 1 && locationKind === FileLocationKind.Absolute) {
|
||||
fileLocation = locationKind;
|
||||
} else if (matcher.fileLocation.length === 2 && locationKind === FileLocationKind.Relative && matcher.fileLocation[1]) {
|
||||
fileLocation = locationKind;
|
||||
filePrefix = matcher.fileLocation[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const locationKind = FileLocationKind.fromString(matcher.fileLocation);
|
||||
if (locationKind) {
|
||||
fileLocation = locationKind;
|
||||
if (locationKind === FileLocationKind.Relative) {
|
||||
filePrefix = '${workspaceFolder}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { fileLocation, filePrefix };
|
||||
}
|
||||
|
||||
// copied from https://github.com/Microsoft/vscode/blob/1.33.1/src/vs/workbench/contrib/tasks/common/problemMatcher.ts
|
||||
private fillDefaults(): void {
|
||||
this.add({
|
||||
name: 'msCompile',
|
||||
label: nls.localizeByDefault('Microsoft compiler problems'),
|
||||
owner: 'msCompile',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: (this.problemPatternRegistry.get('msCompile'))!
|
||||
});
|
||||
|
||||
this.add({
|
||||
name: 'lessCompile',
|
||||
label: nls.localizeByDefault('Less problems'),
|
||||
deprecated: true,
|
||||
owner: 'lessCompile',
|
||||
source: 'less',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: (this.problemPatternRegistry.get('lessCompile'))!,
|
||||
severity: Severity.Error
|
||||
});
|
||||
|
||||
this.add({
|
||||
name: 'gulp-tsc',
|
||||
label: nls.localizeByDefault('Gulp TSC Problems'),
|
||||
owner: 'typescript',
|
||||
source: 'ts',
|
||||
applyTo: ApplyToKind.closedDocuments,
|
||||
fileLocation: FileLocationKind.Relative,
|
||||
filePrefix: '${workspaceFolder}',
|
||||
pattern: (this.problemPatternRegistry.get('gulp-tsc'))!
|
||||
});
|
||||
|
||||
this.add({
|
||||
name: 'jshint',
|
||||
label: nls.localizeByDefault('JSHint problems'),
|
||||
owner: 'jshint',
|
||||
source: 'jshint',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: (this.problemPatternRegistry.get('jshint'))!
|
||||
});
|
||||
|
||||
this.add({
|
||||
name: 'jshint-stylish',
|
||||
label: nls.localizeByDefault('JSHint stylish problems'),
|
||||
owner: 'jshint',
|
||||
source: 'jshint',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: (this.problemPatternRegistry.get('jshint-stylish'))!
|
||||
});
|
||||
|
||||
this.add({
|
||||
name: 'eslint-compact',
|
||||
label: nls.localizeByDefault('ESLint compact problems'),
|
||||
owner: 'eslint',
|
||||
source: 'eslint',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
filePrefix: '${workspaceFolder}',
|
||||
pattern: (this.problemPatternRegistry.get('eslint-compact'))!
|
||||
});
|
||||
|
||||
this.add({
|
||||
name: 'eslint-stylish',
|
||||
label: nls.localizeByDefault('ESLint stylish problems'),
|
||||
owner: 'eslint',
|
||||
source: 'eslint',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: (this.problemPatternRegistry.get('eslint-stylish'))!
|
||||
});
|
||||
|
||||
this.add({
|
||||
name: 'go',
|
||||
label: nls.localizeByDefault('Go problems'),
|
||||
owner: 'go',
|
||||
source: 'go',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Relative,
|
||||
filePrefix: '${workspaceFolder}',
|
||||
pattern: (this.problemPatternRegistry.get('go'))!
|
||||
});
|
||||
}
|
||||
}
|
||||
196
packages/task/src/browser/task-problem-pattern-registry.ts
Normal file
196
packages/task/src/browser/task-problem-pattern-registry.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { NamedProblemPattern, ProblemLocationKind, ProblemPattern, ProblemPatternContribution } from '../common';
|
||||
|
||||
@injectable()
|
||||
export class ProblemPatternRegistry {
|
||||
private readonly patterns = new Map<string, NamedProblemPattern | NamedProblemPattern[]>();
|
||||
private readyPromise = new Deferred<void>();
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.fillDefaults();
|
||||
this.readyPromise.resolve();
|
||||
}
|
||||
|
||||
onReady(): Promise<void> {
|
||||
return this.readyPromise.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a problem pattern to the registry.
|
||||
*
|
||||
* @param definition the problem pattern to be added.
|
||||
*/
|
||||
register(value: ProblemPatternContribution | ProblemPatternContribution[]): Disposable {
|
||||
if (Array.isArray(value)) {
|
||||
const toDispose = new DisposableCollection();
|
||||
value.forEach(problemPatternContribution => toDispose.push(this.register(problemPatternContribution)));
|
||||
return toDispose;
|
||||
}
|
||||
if (!value.name) {
|
||||
console.error('Only named Problem Patterns can be registered.');
|
||||
return Disposable.NULL;
|
||||
}
|
||||
const problemPattern = ProblemPattern.fromProblemPatternContribution(value);
|
||||
return this.add(problemPattern.name!, problemPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the problem pattern(s) from the registry with the given name.
|
||||
*
|
||||
* @param key the name of the problem patterns
|
||||
* @return a problem pattern or an array of the problem patterns associated with the name. If no problem patterns are found, `undefined` is returned.
|
||||
*/
|
||||
get(key: string): undefined | NamedProblemPattern | NamedProblemPattern[] {
|
||||
return this.patterns.get(key);
|
||||
}
|
||||
|
||||
private add(key: string, value: ProblemPattern | ProblemPattern[]): Disposable {
|
||||
let toAdd: NamedProblemPattern | NamedProblemPattern[];
|
||||
if (Array.isArray(value)) {
|
||||
toAdd = value.map(v => Object.assign(v, { name: key }));
|
||||
} else {
|
||||
toAdd = Object.assign(value, { name: key });
|
||||
}
|
||||
this.patterns.set(key, toAdd);
|
||||
return Disposable.create(() => this.patterns.delete(key));
|
||||
}
|
||||
|
||||
// copied from https://github.com/Microsoft/vscode/blob/1.33.1/src/vs/workbench/contrib/tasks/common/problemMatcher.ts
|
||||
private fillDefaults(): void {
|
||||
this.add('msCompile', {
|
||||
regexp: /^(?:\s+\d+\>)?([^\s].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\)\s*:\s+(error|warning|info)\s+(\w{1,2}\d+)\s*:\s*(.*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1,
|
||||
location: 2,
|
||||
severity: 3,
|
||||
code: 4,
|
||||
message: 5
|
||||
});
|
||||
this.add('gulp-tsc', {
|
||||
regexp: /^([^\s].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\):\s+(\d+)\s+(.*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1,
|
||||
location: 2,
|
||||
code: 3,
|
||||
message: 4
|
||||
});
|
||||
this.add('cpp', {
|
||||
regexp: /^([^\s].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\):\s+(error|warning|info)\s+(C\d+)\s*:\s*(.*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1,
|
||||
location: 2,
|
||||
severity: 3,
|
||||
code: 4,
|
||||
message: 5
|
||||
});
|
||||
this.add('csc', {
|
||||
regexp: /^([^\s].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\):\s+(error|warning|info)\s+(CS\d+)\s*:\s*(.*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1,
|
||||
location: 2,
|
||||
severity: 3,
|
||||
code: 4,
|
||||
message: 5
|
||||
});
|
||||
this.add('vb', {
|
||||
regexp: /^([^\s].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\):\s+(error|warning|info)\s+(BC\d+)\s*:\s*(.*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1,
|
||||
location: 2,
|
||||
severity: 3,
|
||||
code: 4,
|
||||
message: 5
|
||||
});
|
||||
this.add('lessCompile', {
|
||||
regexp: /^\s*(.*) in file (.*) line no. (\d+)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
message: 1,
|
||||
file: 2,
|
||||
line: 3
|
||||
});
|
||||
this.add('jshint', {
|
||||
regexp: /^(.*):\s+line\s+(\d+),\s+col\s+(\d+),\s(.+?)(?:\s+\((\w)(\d+)\))?$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1,
|
||||
line: 2,
|
||||
character: 3,
|
||||
message: 4,
|
||||
severity: 5,
|
||||
code: 6
|
||||
});
|
||||
this.add('jshint-stylish', [
|
||||
{
|
||||
regexp: /^(.+)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1
|
||||
},
|
||||
{
|
||||
regexp: /^\s+line\s+(\d+)\s+col\s+(\d+)\s+(.+?)(?:\s+\((\w)(\d+)\))?$/.source,
|
||||
line: 1,
|
||||
character: 2,
|
||||
message: 3,
|
||||
severity: 4,
|
||||
code: 5,
|
||||
loop: true
|
||||
}
|
||||
]);
|
||||
this.add('eslint-compact', {
|
||||
regexp: /^(.+):\sline\s(\d+),\scol\s(\d+),\s(Error|Warning|Info)\s-\s(.+)\s\((.+)\)$/.source,
|
||||
file: 1,
|
||||
kind: ProblemLocationKind.Location,
|
||||
line: 2,
|
||||
character: 3,
|
||||
severity: 4,
|
||||
message: 5,
|
||||
code: 6
|
||||
});
|
||||
this.add('eslint-stylish', [
|
||||
{
|
||||
regexp: /^([^\s].*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1
|
||||
},
|
||||
{
|
||||
regexp: /^\s+(\d+):(\d+)\s+(error|warning|info)\s+(.+?)(?:\s\s+(.*))?$/.source,
|
||||
line: 1,
|
||||
character: 2,
|
||||
severity: 3,
|
||||
message: 4,
|
||||
code: 5,
|
||||
loop: true
|
||||
}
|
||||
]);
|
||||
this.add('go', {
|
||||
regexp: /^([^:]*: )?((.:)?[^:]*):(\d+)(:(\d+))?: (.*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 2,
|
||||
line: 4,
|
||||
character: 6,
|
||||
message: 7
|
||||
});
|
||||
}
|
||||
}
|
||||
700
packages/task/src/browser/task-schema-updater.ts
Normal file
700
packages/task/src/browser/task-schema-updater.ts
Normal file
@@ -0,0 +1,700 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
// This file is inspired by VSCode and partially copied from https://github.com/Microsoft/vscode/blob/1.33.1/src/vs/workbench/contrib/tasks/common/problemMatcher.ts
|
||||
// 'problemMatcher.ts' copyright:
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as Ajv from '@theia/core/shared/ajv';
|
||||
import debounce = require('p-debounce');
|
||||
import { postConstruct, injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { JsonSchemaContribution, JsonSchemaDataStore, JsonSchemaRegisterContext } from '@theia/core/lib/browser/json-schema-store';
|
||||
import { deepClone, Emitter, nls } from '@theia/core/lib/common';
|
||||
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
|
||||
import { inputsSchema } from '@theia/variable-resolver/lib/browser/variable-input-schema';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ProblemMatcherRegistry } from './task-problem-matcher-registry';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import { TaskServer, asVariableName } from '../common';
|
||||
import { UserStorageUri } from '@theia/userstorage/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { JSONObject } from '@theia/core/shared/@lumino/coreutils';
|
||||
import { taskSchemaId } from '../common/task-preferences';
|
||||
|
||||
@injectable()
|
||||
export class TaskSchemaUpdater implements JsonSchemaContribution {
|
||||
|
||||
@inject(JsonSchemaDataStore)
|
||||
protected readonly jsonSchemaData: JsonSchemaDataStore;
|
||||
|
||||
@inject(ProblemMatcherRegistry)
|
||||
protected readonly problemMatcherRegistry: ProblemMatcherRegistry;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(TaskServer)
|
||||
protected readonly taskServer: TaskServer;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
protected readonly onDidChangeTaskSchemaEmitter = new Emitter<void>();
|
||||
readonly onDidChangeTaskSchema = this.onDidChangeTaskSchemaEmitter.event;
|
||||
|
||||
protected readonly uri = new URI(taskSchemaId);
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.jsonSchemaData.setSchema(this.uri, '');
|
||||
this.updateProblemMatcherNames();
|
||||
this.updateSupportedTaskTypes();
|
||||
// update problem matcher names in the task schema every time a problem matcher is added or disposed
|
||||
this.problemMatcherRegistry.onDidChangeProblemMatcher(() => this.updateProblemMatcherNames());
|
||||
// update supported task types in the task schema every time a task definition is registered or removed
|
||||
this.taskDefinitionRegistry.onDidRegisterTaskDefinition(() => this.updateSupportedTaskTypes());
|
||||
this.taskDefinitionRegistry.onDidUnregisterTaskDefinition(() => this.updateSupportedTaskTypes());
|
||||
}
|
||||
|
||||
registerSchemas(context: JsonSchemaRegisterContext): void {
|
||||
context.registerSchema({
|
||||
fileMatch: ['tasks.json', UserStorageUri.resolve('tasks.json').toString()],
|
||||
url: this.uri.toString()
|
||||
});
|
||||
this.workspaceService.updateSchema('tasks', { $ref: this.uri.toString() });
|
||||
}
|
||||
|
||||
readonly update = debounce(() => this.doUpdate(), 0);
|
||||
protected doUpdate(): void {
|
||||
taskConfigurationSchema.anyOf = [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas];
|
||||
|
||||
const schema = this.getTaskSchema();
|
||||
this.doValidate = new Ajv().compile(schema);
|
||||
this.jsonSchemaData.setSchema(this.uri, schema);
|
||||
this.onDidChangeTaskSchemaEmitter.fire(undefined);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
validate(data: any): boolean {
|
||||
return !!this.doValidate && !!this.doValidate(data);
|
||||
}
|
||||
protected doValidate: Ajv.ValidateFunction | undefined;
|
||||
|
||||
/**
|
||||
* Adds given task schema to `taskConfigurationSchema` as `oneOf` subschema.
|
||||
* Replaces existed subschema by given schema if the corresponding `$id` properties are equal.
|
||||
*
|
||||
* Note: please provide `$id` property for subschema to have ability remove/replace it.
|
||||
* @param schema subschema for adding to `taskConfigurationSchema`
|
||||
*/
|
||||
addSubschema(schema: IJSONSchema): void {
|
||||
const schemaId = schema.$id;
|
||||
if (schemaId) {
|
||||
this.doRemoveSubschema(schemaId);
|
||||
}
|
||||
|
||||
customSchemas.push(schema);
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes task subschema from `taskConfigurationSchema`.
|
||||
*
|
||||
* @param arg `$id` property of subschema
|
||||
*/
|
||||
removeSubschema(arg: string): void {
|
||||
const isRemoved = this.doRemoveSubschema(arg);
|
||||
if (isRemoved) {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes task subschema from `customSchemas`, use `update()` to apply the changes for `taskConfigurationSchema`.
|
||||
*
|
||||
* @param arg `$id` property of subschema
|
||||
* @returns `true` if subschema was removed, `false` otherwise
|
||||
*/
|
||||
protected doRemoveSubschema(arg: string): boolean {
|
||||
const index = customSchemas.findIndex(existed => !!existed.$id && existed.$id === arg);
|
||||
if (index > -1) {
|
||||
customSchemas.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns an array of task types that are registered, including the default types */
|
||||
async getRegisteredTaskTypes(): Promise<string[]> {
|
||||
const serverSupportedTypes = await this.taskServer.getRegisteredTaskTypes();
|
||||
const browserSupportedTypes = this.taskDefinitionRegistry.getAll().map(def => def.taskType);
|
||||
const allTypes = new Set([...serverSupportedTypes, ...browserSupportedTypes]);
|
||||
return Array.from(allTypes.values()).sort();
|
||||
}
|
||||
|
||||
private updateSchemasForRegisteredTasks(): void {
|
||||
customizedDetectedTasks.length = 0;
|
||||
const definitions = this.taskDefinitionRegistry.getAll();
|
||||
definitions.forEach(def => {
|
||||
const customizedDetectedTask: IJSONSchema = {
|
||||
type: 'object',
|
||||
required: ['type'],
|
||||
properties: {}
|
||||
};
|
||||
const taskType = {
|
||||
...defaultTaskType,
|
||||
enum: [def.taskType],
|
||||
default: def.taskType,
|
||||
description: nls.localizeByDefault('The task type to customize')
|
||||
};
|
||||
customizedDetectedTask.properties!.type = taskType;
|
||||
const required = def.properties.required || [];
|
||||
def.properties.all.forEach(taskProp => {
|
||||
if (required.find(requiredProp => requiredProp === taskProp)) { // property is mandatory
|
||||
customizedDetectedTask.required!.push(taskProp);
|
||||
}
|
||||
customizedDetectedTask.properties![taskProp] = { ...def.properties.schema.properties![taskProp] };
|
||||
});
|
||||
customizedDetectedTask.properties!.label = taskLabel;
|
||||
customizedDetectedTask.properties!.problemMatcher = problemMatcher;
|
||||
customizedDetectedTask.properties!.presentation = presentation;
|
||||
customizedDetectedTask.properties!.options = commandOptionsSchema;
|
||||
customizedDetectedTask.properties!.group = group;
|
||||
customizedDetectedTask.properties!.detail = detail;
|
||||
customizedDetectedTask.additionalProperties = true;
|
||||
customizedDetectedTasks.push(customizedDetectedTask);
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns the task's JSON schema */
|
||||
getTaskSchema(): IJSONSchema & { default: JSONObject } {
|
||||
return {
|
||||
type: 'object',
|
||||
default: { version: '2.0.0', tasks: [] },
|
||||
properties: {
|
||||
version: {
|
||||
type: 'string',
|
||||
default: '2.0.0',
|
||||
description: nls.localizeByDefault("The config's version number.")
|
||||
},
|
||||
tasks: {
|
||||
type: 'array',
|
||||
items: {
|
||||
...deepClone(taskConfigurationSchema)
|
||||
},
|
||||
description: nls.localizeByDefault('The task configurations. Usually these are enrichments of task already defined in the external task runner.')
|
||||
},
|
||||
inputs: inputsSchema.definitions!.inputs
|
||||
},
|
||||
additionalProperties: false,
|
||||
allowComments: true,
|
||||
allowTrailingCommas: true,
|
||||
};
|
||||
}
|
||||
|
||||
/** Gets the most up-to-date names of problem matchers from the registry and update the task schema */
|
||||
private updateProblemMatcherNames(): void {
|
||||
const matcherNames = this.problemMatcherRegistry.getAll().map(m => asVariableName(m.name));
|
||||
problemMatcherNames.length = 0;
|
||||
problemMatcherNames.push(...matcherNames);
|
||||
this.update();
|
||||
}
|
||||
|
||||
private async updateSupportedTaskTypes(): Promise<void> {
|
||||
this.updateSchemasForRegisteredTasks();
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
const commandSchema: IJSONSchema = {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('The command to be executed. Can be an external program or a shell command.')
|
||||
};
|
||||
|
||||
const commandArgSchema: IJSONSchema = {
|
||||
type: 'array',
|
||||
description: nls.localizeByDefault('Arguments passed to the command when this task is invoked.'),
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const commandOptionsSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('Additional command options'),
|
||||
properties: {
|
||||
cwd: {
|
||||
type: 'string',
|
||||
description: nls.localize('theia/task/schema/commandOptions/cwd',
|
||||
"The current working directory of the executed program or script. If omitted Theia's current workspace root is used."),
|
||||
default: '${workspaceFolder}'
|
||||
},
|
||||
env: {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault("The environment of the executed program or shell. If omitted the parent process' environment is used.")
|
||||
},
|
||||
shell: {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('Configures the shell to be used.'),
|
||||
properties: {
|
||||
executable: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('The shell to be used.')
|
||||
},
|
||||
args: {
|
||||
type: 'array',
|
||||
description: nls.localizeByDefault('The shell arguments.'),
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const problemMatcherNames: string[] = [];
|
||||
const defaultTaskTypes = ['shell', 'process'];
|
||||
const supportedTaskTypes = [...defaultTaskTypes];
|
||||
const taskLabel: IJSONSchema = {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault("The task's user interface label")
|
||||
};
|
||||
const defaultTaskType: IJSONSchema = {
|
||||
type: 'string',
|
||||
enum: supportedTaskTypes,
|
||||
default: defaultTaskTypes[0],
|
||||
description: nls.localizeByDefault('Defines whether the task is run as a process or as a command inside a shell.')
|
||||
} as const;
|
||||
const commandAndArgs = {
|
||||
command: commandSchema,
|
||||
args: commandArgSchema,
|
||||
options: commandOptionsSchema
|
||||
};
|
||||
|
||||
const group: IJSONSchema = {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
enum: ['build', 'test', 'none'],
|
||||
enumDescriptions: [
|
||||
nls.localizeByDefault("Marks the task as a build task accessible through the 'Run Build Task' command."),
|
||||
nls.localizeByDefault("Marks the task as a test task accessible through the 'Run Test Task' command."),
|
||||
nls.localizeByDefault('Assigns the task to no group')
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
kind: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
description: nls.localizeByDefault("The task's execution group."),
|
||||
enum: ['build', 'test', 'none'],
|
||||
enumDescriptions: [
|
||||
nls.localizeByDefault("Marks the task as a build task accessible through the 'Run Build Task' command."),
|
||||
nls.localizeByDefault("Marks the task as a test task accessible through the 'Run Test Task' command."),
|
||||
nls.localizeByDefault('Assigns the task to no group')
|
||||
]
|
||||
},
|
||||
isDefault: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localizeByDefault('Defines if this task is the default task in the group, or a glob to match the file which should trigger this task.')
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
description: nls.localizeByDefault(
|
||||
'Defines to which execution group this task belongs to. It supports "build" to add it to the build group and "test" to add it to the test group.')
|
||||
};
|
||||
|
||||
const problemPattern: IJSONSchema = {
|
||||
default: {
|
||||
regexp: '^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$',
|
||||
file: 1,
|
||||
location: 2,
|
||||
message: 3
|
||||
},
|
||||
type: 'object',
|
||||
properties: {
|
||||
regexp: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('The regular expression to find an error, warning or info in the output.')
|
||||
},
|
||||
kind: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('whether the pattern matches a location (file and line) or only a file.')
|
||||
},
|
||||
file: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault('The match group index of the filename. If omitted 1 is used.')
|
||||
},
|
||||
location: {
|
||||
type: 'integer',
|
||||
// eslint-disable-next-line max-len
|
||||
description: nls.localizeByDefault("The match group index of the problem's location. Valid location patterns are: (line), (line,column) and (startLine,startColumn,endLine,endColumn). If omitted (line,column) is assumed.")
|
||||
},
|
||||
line: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault("The match group index of the problem's line. Defaults to 2")
|
||||
},
|
||||
column: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault("The match group index of the problem's line character. Defaults to 3")
|
||||
},
|
||||
endLine: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault("The match group index of the problem's end line. Defaults to undefined")
|
||||
},
|
||||
endColumn: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault("The match group index of the problem's end line character. Defaults to undefined")
|
||||
},
|
||||
severity: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault("The match group index of the problem's severity. Defaults to undefined")
|
||||
},
|
||||
code: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault("The match group index of the problem's code. Defaults to undefined")
|
||||
},
|
||||
message: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault('The match group index of the message. If omitted it defaults to 4 if location is specified. Otherwise it defaults to 5.')
|
||||
},
|
||||
loop: {
|
||||
type: 'boolean',
|
||||
// eslint-disable-next-line max-len
|
||||
description: nls.localizeByDefault('In a multi line matcher loop indicated whether this pattern is executed in a loop as long as it matches. Can only specified on a last pattern in a multi line pattern.')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const multiLineProblemPattern: IJSONSchema = {
|
||||
type: 'array',
|
||||
items: problemPattern
|
||||
};
|
||||
|
||||
const watchingPattern: IJSONSchema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
regexp: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('The regular expression to detect the begin or end of a background task.')
|
||||
},
|
||||
file: {
|
||||
type: 'integer',
|
||||
description: nls.localizeByDefault('The match group index of the filename. Can be omitted.')
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const patternType: IJSONSchema = {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('The name of a contributed or predefined pattern')
|
||||
},
|
||||
problemPattern,
|
||||
multiLineProblemPattern
|
||||
],
|
||||
description: nls.localizeByDefault('A problem pattern or the name of a contributed or predefined problem pattern. Can be omitted if base is specified.')
|
||||
};
|
||||
|
||||
const problemMatcherObject: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
base: {
|
||||
type: 'string',
|
||||
enum: problemMatcherNames,
|
||||
description: nls.localizeByDefault('The name of a base problem matcher to use.')
|
||||
},
|
||||
owner: {
|
||||
type: 'string',
|
||||
description: nls.localize('theia/task/schema/problemMatcherObject/owner',
|
||||
"The owner of the problem inside Theia. Can be omitted if base is specified. Defaults to 'external' if omitted and base is not specified.")
|
||||
},
|
||||
source: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault("A human-readable string describing the source of this diagnostic, e.g. 'typescript' or 'super lint'.")
|
||||
},
|
||||
severity: {
|
||||
type: 'string',
|
||||
enum: ['error', 'warning', 'info'],
|
||||
description: nls.localizeByDefault("The default severity for captures problems. Is used if the pattern doesn't define a match group for severity.")
|
||||
},
|
||||
applyTo: {
|
||||
type: 'string',
|
||||
enum: ['allDocuments', 'openDocuments', 'closedDocuments'],
|
||||
description: nls.localizeByDefault('Controls if a problem reported on a text document is applied only to open, closed or all documents.')
|
||||
},
|
||||
pattern: patternType,
|
||||
fileLocation: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
enum: ['absolute', 'relative', 'autoDetect']
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
],
|
||||
// eslint-disable-next-line max-len
|
||||
description: nls.localizeByDefault('Defines how file names reported in a problem pattern should be interpreted. A relative fileLocation may be an array, where the second element of the array is the path of the relative file location. The search fileLocation mode, performs a deep (and, possibly, heavy) file system search within the directories specified by the include/exclude properties of the second element (or the current workspace directory if not specified).')
|
||||
},
|
||||
background: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
description: nls.localizeByDefault('Patterns to track the begin and end of a matcher active on a background task.'),
|
||||
properties: {
|
||||
activeOnStart: {
|
||||
type: 'boolean',
|
||||
description: nls.localizeByDefault(
|
||||
'If set to true the background monitor starts in active mode. This is the same as outputting a line that matches beginsPattern when the task starts.')
|
||||
},
|
||||
beginsPattern: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
watchingPattern
|
||||
],
|
||||
description: nls.localizeByDefault('If matched in the output the start of a background task is signaled.')
|
||||
},
|
||||
endsPattern: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
watchingPattern
|
||||
],
|
||||
description: nls.localizeByDefault('If matched in the output the end of a background task is signaled.')
|
||||
}
|
||||
}
|
||||
},
|
||||
watching: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
deprecationMessage: nls.localizeByDefault('The watching property is deprecated. Use background instead.'),
|
||||
description: nls.localizeByDefault('Patterns to track the begin and end of a watching matcher.'),
|
||||
properties: {
|
||||
activeOnStart: {
|
||||
type: 'boolean',
|
||||
description: nls.localizeByDefault(
|
||||
'If set to true the watcher starts in active mode. This is the same as outputting a line that matches beginsPattern when the task starts.')
|
||||
},
|
||||
beginsPattern: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
watchingPattern
|
||||
],
|
||||
description: nls.localizeByDefault('If matched in the output the start of a watching task is signaled.')
|
||||
},
|
||||
endsPattern: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
watchingPattern
|
||||
],
|
||||
description: nls.localizeByDefault('If matched in the output the end of a watching task is signaled.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const problemMatcher: IJSONSchema = {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
enum: problemMatcherNames
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: problemMatcherNames
|
||||
}
|
||||
},
|
||||
problemMatcherObject,
|
||||
{
|
||||
type: 'array',
|
||||
items: problemMatcherObject
|
||||
}
|
||||
],
|
||||
description: nls.localizeByDefault('The problem matcher(s) to use. Can either be a string or a problem matcher definition or an array of strings and problem matchers.')
|
||||
};
|
||||
|
||||
const presentation: IJSONSchema = {
|
||||
type: 'object',
|
||||
default: {
|
||||
echo: true,
|
||||
reveal: 'always',
|
||||
focus: false,
|
||||
panel: 'shared',
|
||||
showReuseMessage: true,
|
||||
clear: false
|
||||
},
|
||||
description: nls.localizeByDefault("Configures the panel that is used to present the task's output and reads its input."),
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
echo: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: nls.localizeByDefault('Controls whether the executed command is echoed to the panel. Default is true.')
|
||||
},
|
||||
focus: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localizeByDefault('Controls whether the panel takes focus. Default is false. If set to true the panel is revealed as well.')
|
||||
},
|
||||
reveal: {
|
||||
type: 'string',
|
||||
enum: ['always', 'silent', 'never'],
|
||||
enumDescriptions: [
|
||||
nls.localizeByDefault('Always reveals the terminal when this task is executed.'),
|
||||
nls.localizeByDefault('Only reveals the terminal if the task exits with an error or the problem matcher finds an error.'),
|
||||
nls.localizeByDefault('Never reveals the terminal when this task is executed.')
|
||||
],
|
||||
default: 'always',
|
||||
description: nls.localizeByDefault(
|
||||
'Controls whether the terminal running the task is revealed or not. May be overridden by option "revealProblems". Default is "always".')
|
||||
},
|
||||
panel: {
|
||||
type: 'string',
|
||||
enum: ['shared', 'dedicated', 'new'],
|
||||
enumDescriptions: [
|
||||
nls.localize('theia/task/schema/presentation/panel/shared', 'The terminal is shared and the output of other task runs are added to the same terminal.'),
|
||||
// eslint-disable-next-line max-len
|
||||
nls.localize('theia/task/schema/presentation/panel/dedicated', 'The terminal is dedicated to a specific task. If that task is executed again, the terminal is reused. However, the output of a different task is presented in a different terminal.'),
|
||||
nls.localize('theia/task/schema/presentation/panel/new', 'Every execution of that task is using a new clean terminal.')
|
||||
],
|
||||
default: 'shared',
|
||||
description: nls.localizeByDefault('Controls if the panel is shared between tasks, dedicated to this task or a new one is created on every run.')
|
||||
},
|
||||
showReuseMessage: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: nls.localize('theia/task/schema/presentation/showReuseMessage', 'Controls whether to show the "Terminal will be reused by tasks" message.')
|
||||
},
|
||||
clear: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localizeByDefault('Controls whether the terminal is cleared before executing the task.')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const detail: IJSONSchema = {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('An optional description of a task that shows in the Run Task quick pick as a detail.')
|
||||
};
|
||||
|
||||
const taskIdentifier: IJSONSchema = {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('The task identifier.')
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const processTaskConfigurationSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
required: ['type', 'label', 'command'],
|
||||
properties: {
|
||||
label: taskLabel,
|
||||
type: defaultTaskType,
|
||||
...commandAndArgs,
|
||||
isBackground: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localizeByDefault('Whether the executed task is kept alive and is running in the background.')
|
||||
},
|
||||
dependsOn: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('Another task this task depends on.')
|
||||
},
|
||||
taskIdentifier,
|
||||
{
|
||||
type: 'array',
|
||||
description: nls.localizeByDefault('The other tasks this task depends on.'),
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
taskIdentifier
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
description: nls.localizeByDefault('Either a string representing another task or an array of other tasks that this task depends on.')
|
||||
},
|
||||
dependsOrder: {
|
||||
type: 'string',
|
||||
enum: ['parallel', 'sequence'],
|
||||
enumDescriptions: [
|
||||
nls.localizeByDefault('Run all dependsOn tasks in parallel.'),
|
||||
nls.localizeByDefault('Run all dependsOn tasks in sequence.')
|
||||
],
|
||||
default: 'parallel',
|
||||
description: nls.localizeByDefault('Determines the order of the dependsOn tasks for this task. Note that this property is not recursive.')
|
||||
},
|
||||
windows: {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('Windows specific command configuration'),
|
||||
properties: commandAndArgs
|
||||
},
|
||||
osx: {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('Mac specific command configuration'),
|
||||
properties: commandAndArgs
|
||||
},
|
||||
linux: {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('Linux specific command configuration'),
|
||||
properties: commandAndArgs
|
||||
},
|
||||
group,
|
||||
problemMatcher,
|
||||
presentation,
|
||||
detail,
|
||||
},
|
||||
additionalProperties: true
|
||||
};
|
||||
|
||||
const customizedDetectedTasks: IJSONSchema[] = [];
|
||||
const customSchemas: IJSONSchema[] = [];
|
||||
|
||||
const taskConfigurationSchema: IJSONSchema = {
|
||||
$id: taskSchemaId,
|
||||
anyOf: [processTaskConfigurationSchema, ...customizedDetectedTasks, ...customSchemas]
|
||||
};
|
||||
1167
packages/task/src/browser/task-service.ts
Normal file
1167
packages/task/src/browser/task-service.ts
Normal file
File diff suppressed because it is too large
Load Diff
36
packages/task/src/browser/task-source-resolver.ts
Normal file
36
packages/task/src/browser/task-source-resolver.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { TaskConfiguration, TaskScope } from '../common';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
|
||||
@injectable()
|
||||
export class TaskSourceResolver {
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
/**
|
||||
* Returns task source to display.
|
||||
*/
|
||||
resolve(task: TaskConfiguration): string {
|
||||
if (typeof task._scope === 'string') {
|
||||
return task._scope;
|
||||
} else {
|
||||
return TaskScope[task._scope];
|
||||
}
|
||||
}
|
||||
}
|
||||
169
packages/task/src/browser/task-templates.ts
Normal file
169
packages/task/src/browser/task-templates.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { QuickPickValue } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
/** The representation of a task template used in the auto-generation of `tasks.json` */
|
||||
export interface TaskTemplateEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
sort?: string; // string used in the sorting. If `undefined` the label is used in sorting.
|
||||
autoDetect: boolean; // not supported in Theia
|
||||
content: string;
|
||||
}
|
||||
|
||||
const dotnetBuild: TaskTemplateEntry = {
|
||||
id: 'dotnetCore',
|
||||
label: '.NET Core',
|
||||
sort: 'NET Core',
|
||||
autoDetect: false, // not supported in Theia
|
||||
description: nls.localizeByDefault('Executes .NET Core build command'),
|
||||
content: [
|
||||
'{',
|
||||
'\t// See https://go.microsoft.com/fwlink/?LinkId=733558',
|
||||
'\t// for the documentation about the tasks.json format',
|
||||
'\t"version": "2.0.0",',
|
||||
'\t"tasks": [',
|
||||
'\t\t{',
|
||||
'\t\t\t"label": "build",',
|
||||
'\t\t\t"command": "dotnet",',
|
||||
'\t\t\t"type": "shell",',
|
||||
'\t\t\t"args": [',
|
||||
'\t\t\t\t"build",',
|
||||
'\t\t\t\t// Ask dotnet build to generate full paths for file names.',
|
||||
'\t\t\t\t"/property:GenerateFullPaths=true",',
|
||||
'\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel',
|
||||
'\t\t\t\t"/consoleloggerparameters:NoSummary"',
|
||||
'\t\t\t],',
|
||||
'\t\t\t"group": "build",',
|
||||
'\t\t\t"presentation": {',
|
||||
'\t\t\t\t"reveal": "silent"',
|
||||
'\t\t\t},',
|
||||
'\t\t\t"problemMatcher": "$msCompile"',
|
||||
'\t\t}',
|
||||
'\t]',
|
||||
'}'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
const msbuild: TaskTemplateEntry = {
|
||||
id: 'msbuild',
|
||||
label: 'MSBuild',
|
||||
autoDetect: false, // not supported in Theia
|
||||
description: nls.localizeByDefault('Executes the build target'),
|
||||
content: [
|
||||
'{',
|
||||
'\t// See https://go.microsoft.com/fwlink/?LinkId=733558',
|
||||
'\t// for the documentation about the tasks.json format',
|
||||
'\t"version": "2.0.0",',
|
||||
'\t"tasks": [',
|
||||
'\t\t{',
|
||||
'\t\t\t"label": "build",',
|
||||
'\t\t\t"type": "shell",',
|
||||
'\t\t\t"command": "msbuild",',
|
||||
'\t\t\t"args": [',
|
||||
'\t\t\t\t// Ask msbuild to generate full paths for file names.',
|
||||
'\t\t\t\t"/property:GenerateFullPaths=true",',
|
||||
'\t\t\t\t"/t:build",',
|
||||
'\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel',
|
||||
'\t\t\t\t"/consoleloggerparameters:NoSummary"',
|
||||
'\t\t\t],',
|
||||
'\t\t\t"group": "build",',
|
||||
'\t\t\t"presentation": {',
|
||||
'\t\t\t\t// Reveal the output only if unrecognized errors occur.',
|
||||
'\t\t\t\t"reveal": "silent"',
|
||||
'\t\t\t},',
|
||||
'\t\t\t// Use the standard MS compiler pattern to detect errors, warnings and infos',
|
||||
'\t\t\t"problemMatcher": "$msCompile"',
|
||||
'\t\t}',
|
||||
'\t]',
|
||||
'}'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
const maven: TaskTemplateEntry = {
|
||||
id: 'maven',
|
||||
label: 'maven',
|
||||
sort: 'MVN',
|
||||
autoDetect: false, // not supported in Theia
|
||||
description: nls.localizeByDefault('Executes common maven commands'),
|
||||
content: [
|
||||
'{',
|
||||
'\t// See https://go.microsoft.com/fwlink/?LinkId=733558',
|
||||
'\t// for the documentation about the tasks.json format',
|
||||
'\t"version": "2.0.0",',
|
||||
'\t"tasks": [',
|
||||
'\t\t{',
|
||||
'\t\t\t"label": "verify",',
|
||||
'\t\t\t"type": "shell",',
|
||||
'\t\t\t"command": "mvn -B verify",',
|
||||
'\t\t\t"group": "build"',
|
||||
'\t\t},',
|
||||
'\t\t{',
|
||||
'\t\t\t"label": "test",',
|
||||
'\t\t\t"type": "shell",',
|
||||
'\t\t\t"command": "mvn -B test",',
|
||||
'\t\t\t"group": "test"',
|
||||
'\t\t}',
|
||||
'\t]',
|
||||
'}'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
const command: TaskTemplateEntry = {
|
||||
id: 'externalCommand',
|
||||
label: 'Others',
|
||||
autoDetect: false, // not supported in Theia
|
||||
description: nls.localizeByDefault('Example to run an arbitrary external command'),
|
||||
content: [
|
||||
'{',
|
||||
'\t// See https://go.microsoft.com/fwlink/?LinkId=733558',
|
||||
'\t// for the documentation about the tasks.json format',
|
||||
'\t"version": "2.0.0",',
|
||||
'\t"tasks": [',
|
||||
'\t\t{',
|
||||
'\t\t\t"label": "echo",',
|
||||
'\t\t\t"type": "shell",',
|
||||
'\t\t\t"command": "echo Hello"',
|
||||
'\t\t}',
|
||||
'\t]',
|
||||
'}'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class TaskTemplateSelector {
|
||||
selectTemplates(): QuickPickValue<TaskTemplateEntry>[] {
|
||||
const templates: TaskTemplateEntry[] = [
|
||||
dotnetBuild, msbuild, maven
|
||||
].sort((a, b) =>
|
||||
(a.sort || a.label).localeCompare(b.sort || b.label)
|
||||
);
|
||||
templates.push(command);
|
||||
return templates.map(t => ({
|
||||
label: t.label,
|
||||
description: t.description,
|
||||
value: t
|
||||
}));
|
||||
}
|
||||
}
|
||||
225
packages/task/src/browser/task-terminal-widget-manager.ts
Normal file
225
packages/task/src/browser/task-terminal-widget-manager.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser';
|
||||
import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
|
||||
import { TerminalWidgetFactoryOptions } from '@theia/terminal/lib/browser/terminal-widget-impl';
|
||||
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
|
||||
import { PanelKind, TaskConfiguration, TaskWatcher, TaskExitedEvent, TaskServer, TaskOutputPresentation, TaskInfo } from '../common';
|
||||
import { ProcessTaskInfo } from '../common/process/task-protocol';
|
||||
import { TaskDefinitionRegistry } from './task-definition-registry';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
export interface TaskTerminalWidget extends TerminalWidget {
|
||||
readonly kind: 'task';
|
||||
dedicated?: boolean;
|
||||
taskId?: number;
|
||||
taskConfig?: TaskConfiguration;
|
||||
busy?: boolean;
|
||||
}
|
||||
export namespace TaskTerminalWidget {
|
||||
export function is(widget: TerminalWidget): widget is TaskTerminalWidget {
|
||||
return widget.kind === 'task';
|
||||
}
|
||||
}
|
||||
|
||||
export interface TaskTerminalWidgetOpenerOptions extends WidgetOpenerOptions {
|
||||
taskConfig?: TaskConfiguration;
|
||||
taskInfo?: TaskInfo;
|
||||
}
|
||||
export namespace TaskTerminalWidgetOpenerOptions {
|
||||
export function isDedicatedTerminal(options: TaskTerminalWidgetOpenerOptions): boolean {
|
||||
const taskConfig = options.taskInfo ? options.taskInfo.config : options.taskConfig;
|
||||
return !!taskConfig && !!taskConfig.presentation && taskConfig.presentation.panel === PanelKind.Dedicated;
|
||||
}
|
||||
|
||||
export function isNewTerminal(options: TaskTerminalWidgetOpenerOptions): boolean {
|
||||
const taskConfig = options.taskInfo ? options.taskInfo.config : options.taskConfig;
|
||||
return !!taskConfig && !!taskConfig.presentation && taskConfig.presentation.panel === PanelKind.New;
|
||||
}
|
||||
|
||||
export function isSharedTerminal(options: TaskTerminalWidgetOpenerOptions): boolean {
|
||||
const taskConfig = options.taskInfo ? options.taskInfo.config : options.taskConfig;
|
||||
return !!taskConfig && (taskConfig.presentation === undefined || taskConfig.presentation.panel === undefined || taskConfig.presentation.panel === PanelKind.Shared);
|
||||
}
|
||||
|
||||
export function echoExecutedCommand(options: TaskTerminalWidgetOpenerOptions): boolean {
|
||||
const taskConfig = options.taskInfo ? options.taskInfo.config : options.taskConfig;
|
||||
return !!taskConfig && (taskConfig.presentation === undefined || taskConfig.presentation.echo === undefined || taskConfig.presentation.echo);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class TaskTerminalWidgetManager {
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(TaskDefinitionRegistry)
|
||||
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
|
||||
|
||||
@inject(TerminalService)
|
||||
protected readonly terminalService: TerminalService;
|
||||
|
||||
@inject(TaskWatcher)
|
||||
protected readonly taskWatcher: TaskWatcher;
|
||||
|
||||
@inject(TaskServer)
|
||||
protected readonly taskServer: TaskServer;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.taskWatcher.onTaskExit((event: TaskExitedEvent) => {
|
||||
const finishedTaskId = event.taskId;
|
||||
// find the terminal where the task ran, and mark it as "idle"
|
||||
for (const terminal of this.getTaskTerminalWidgets()) {
|
||||
if (terminal.taskId === finishedTaskId) {
|
||||
const showReuseMessage = !!event.config && TaskOutputPresentation.shouldShowReuseMessage(event.config);
|
||||
const closeOnFinish = !!event.config && TaskOutputPresentation.shouldCloseTerminalOnFinish(event.config);
|
||||
this.updateTerminalOnTaskExit(terminal, showReuseMessage, closeOnFinish);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.terminalService.onDidCreateTerminal(async (widget: TerminalWidget) => {
|
||||
const terminal = TaskTerminalWidget.is(widget) && widget;
|
||||
if (terminal) {
|
||||
const didConnectListener = terminal.onDidOpen(async () => {
|
||||
const context = this.workspaceService?.workspace?.resource.toString();
|
||||
const tasksInfo = await this.taskServer.getTasks(context);
|
||||
const taskInfo = tasksInfo.find(info => info.terminalId === widget.terminalId);
|
||||
if (taskInfo) {
|
||||
const taskConfig = taskInfo.config;
|
||||
terminal.dedicated = !!taskConfig.presentation && !!taskConfig.presentation.panel && taskConfig.presentation.panel === PanelKind.Dedicated;
|
||||
terminal.taskId = taskInfo.taskId;
|
||||
terminal.taskConfig = taskConfig;
|
||||
terminal.busy = true;
|
||||
} else {
|
||||
this.updateTerminalOnTaskExit(terminal, true, false);
|
||||
}
|
||||
});
|
||||
const didConnectFailureListener = terminal.onDidOpenFailure(async () => {
|
||||
this.updateTerminalOnTaskExit(terminal, true, false);
|
||||
});
|
||||
terminal.onDidDispose(() => {
|
||||
didConnectListener.dispose();
|
||||
didConnectFailureListener.dispose();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async newTaskTerminal(factoryOptions: TerminalWidgetFactoryOptions): Promise<TerminalWidget> {
|
||||
return this.terminalService.newTerminal({ ...factoryOptions, kind: 'task' });
|
||||
}
|
||||
|
||||
async open(factoryOptions: TerminalWidgetFactoryOptions, openerOptions: TaskTerminalWidgetOpenerOptions): Promise<TerminalWidget> {
|
||||
const taskInfo = openerOptions.taskInfo;
|
||||
const taskConfig = taskInfo ? taskInfo.config : openerOptions.taskConfig;
|
||||
const dedicated = TaskTerminalWidgetOpenerOptions.isDedicatedTerminal(openerOptions);
|
||||
if (dedicated && !taskConfig) {
|
||||
throw new Error('"taskConfig" must be included as part of the "option.taskInfo" if "isDedicated" is true');
|
||||
}
|
||||
|
||||
const { isNew, widget } = await this.getWidgetToRunTask(factoryOptions, openerOptions);
|
||||
if (isNew) {
|
||||
this.shell.addWidget(widget, { area: openerOptions.widgetOptions ? openerOptions.widgetOptions.area : 'bottom' });
|
||||
widget.resetTerminal();
|
||||
} else {
|
||||
if (factoryOptions.title) {
|
||||
widget.setTitle(factoryOptions.title);
|
||||
}
|
||||
if (taskConfig && TaskOutputPresentation.shouldClearTerminalBeforeRun(taskConfig)) {
|
||||
widget.clearOutput();
|
||||
}
|
||||
}
|
||||
this.terminalService.open(widget, openerOptions);
|
||||
|
||||
if (TaskTerminalWidgetOpenerOptions.echoExecutedCommand(openerOptions) &&
|
||||
taskInfo && ProcessTaskInfo.is(taskInfo) && taskInfo.command && taskInfo.command.length > 0
|
||||
) {
|
||||
widget.writeLine('\x1b[1m> ' + nls.localizeByDefault('Executing task: {0}', taskInfo.command) + ' <\x1b[0m\n');
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
protected async getWidgetToRunTask(
|
||||
factoryOptions: TerminalWidgetFactoryOptions, openerOptions: TaskTerminalWidgetOpenerOptions
|
||||
): Promise<{ isNew: boolean, widget: TerminalWidget }> {
|
||||
let reusableTerminalWidget: TerminalWidget | undefined;
|
||||
const taskConfig = openerOptions.taskInfo ? openerOptions.taskInfo.config : openerOptions.taskConfig;
|
||||
if (TaskTerminalWidgetOpenerOptions.isDedicatedTerminal(openerOptions)) {
|
||||
for (const widget of this.getTaskTerminalWidgets()) {
|
||||
// to run a task whose `taskPresentation === 'dedicated'`, the terminal to be reused must be
|
||||
// 1) dedicated, 2) idle, 3) the one that ran the same task
|
||||
if (widget.dedicated &&
|
||||
!widget.busy &&
|
||||
widget.taskConfig && taskConfig &&
|
||||
this.taskDefinitionRegistry.compareTasks(taskConfig, widget.taskConfig)) {
|
||||
|
||||
reusableTerminalWidget = widget;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (TaskTerminalWidgetOpenerOptions.isSharedTerminal(openerOptions)) {
|
||||
const availableWidgets: TerminalWidget[] = [];
|
||||
for (const widget of this.getTaskTerminalWidgets()) {
|
||||
// to run a task whose `taskPresentation === 'shared'`, the terminal to be used must be
|
||||
// 1) not dedicated, and 2) idle
|
||||
if (!widget.dedicated && !widget.busy) {
|
||||
availableWidgets.push(widget);
|
||||
}
|
||||
}
|
||||
const lastUsedWidget = availableWidgets.find(w => {
|
||||
const lastUsedTerminal = this.terminalService.lastUsedTerminal;
|
||||
return lastUsedTerminal && lastUsedTerminal.id === w.id;
|
||||
});
|
||||
reusableTerminalWidget = lastUsedWidget || availableWidgets[0];
|
||||
}
|
||||
|
||||
// we are unable to find a terminal widget to run the task, or `taskPresentation === 'new'`
|
||||
const lastCwd = taskConfig?.options?.cwd ? new URI(taskConfig.options.cwd) : new URI();
|
||||
|
||||
if (!reusableTerminalWidget) {
|
||||
const widget = await this.newTaskTerminal(factoryOptions);
|
||||
widget.lastCwd = lastCwd;
|
||||
return { isNew: true, widget };
|
||||
}
|
||||
reusableTerminalWidget.lastCwd = lastCwd;
|
||||
return { isNew: false, widget: reusableTerminalWidget };
|
||||
}
|
||||
|
||||
protected getTaskTerminalWidgets(): TaskTerminalWidget[] {
|
||||
return this.terminalService.all.filter(TaskTerminalWidget.is);
|
||||
}
|
||||
|
||||
protected updateTerminalOnTaskExit(terminal: TaskTerminalWidget, showReuseMessage: boolean, closeOnFinish: boolean): void {
|
||||
terminal.busy = false;
|
||||
if (closeOnFinish) {
|
||||
terminal.close();
|
||||
} else if (showReuseMessage) {
|
||||
terminal.scrollToBottom();
|
||||
terminal.writeLine('\x1b[1m\n\r' + nls.localize('theia/task/terminalWillBeReusedByTasks', 'Terminal will be reused by tasks.') + '\x1b[0m\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
27
packages/task/src/browser/tasks-monaco-contribution.ts
Normal file
27
packages/task/src/browser/tasks-monaco-contribution.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
monaco.languages.register({
|
||||
id: 'jsonc',
|
||||
'aliases': [
|
||||
'JSON with Comments'
|
||||
],
|
||||
'filenames': [
|
||||
'tasks.json'
|
||||
]
|
||||
});
|
||||
20
packages/task/src/common/index.ts
Normal file
20
packages/task/src/common/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './task-protocol';
|
||||
export * from './task-watcher';
|
||||
export * from './problem-matcher-protocol';
|
||||
export * from './task-util';
|
||||
234
packages/task/src/common/problem-matcher-protocol.ts
Normal file
234
packages/task/src/common/problem-matcher-protocol.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// This file is inspired by VSCode https://github.com/Microsoft/vscode/blob/1.33.1/src/vs/workbench/contrib/tasks/common/problemMatcher.ts
|
||||
// 'problemMatcher.ts' copyright:
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '@theia/core';
|
||||
import { Severity } from '@theia/core/lib/common/severity';
|
||||
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
|
||||
export enum ApplyToKind {
|
||||
allDocuments,
|
||||
openDocuments,
|
||||
closedDocuments
|
||||
}
|
||||
|
||||
export namespace ApplyToKind {
|
||||
export function fromString(value: string | undefined): ApplyToKind | undefined {
|
||||
if (value) {
|
||||
value = value.toLowerCase();
|
||||
if (value === 'alldocuments') {
|
||||
return ApplyToKind.allDocuments;
|
||||
} else if (value === 'opendocuments') {
|
||||
return ApplyToKind.openDocuments;
|
||||
} else if (value === 'closeddocuments') {
|
||||
return ApplyToKind.closedDocuments;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export enum FileLocationKind {
|
||||
Auto,
|
||||
Relative,
|
||||
Absolute
|
||||
}
|
||||
|
||||
export namespace FileLocationKind {
|
||||
export function fromString(value: string): FileLocationKind | undefined {
|
||||
value = value.toLowerCase();
|
||||
if (value === 'absolute') {
|
||||
return FileLocationKind.Absolute;
|
||||
} else if (value === 'relative') {
|
||||
return FileLocationKind.Relative;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface WatchingPattern {
|
||||
regexp: string;
|
||||
file?: number;
|
||||
}
|
||||
|
||||
export interface WatchingMatcher {
|
||||
// If set to true the background monitor is in active mode when the task starts.
|
||||
// This is equals of issuing a line that matches the beginPattern
|
||||
activeOnStart: boolean;
|
||||
beginsPattern: WatchingPattern;
|
||||
endsPattern: WatchingPattern;
|
||||
}
|
||||
export namespace WatchingMatcher {
|
||||
export function fromWatchingMatcherContribution(value: WatchingMatcherContribution | undefined): WatchingMatcher | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
activeOnStart: !!value.activeOnStart,
|
||||
beginsPattern: typeof value.beginsPattern === 'string' ? { regexp: value.beginsPattern } : value.beginsPattern,
|
||||
endsPattern: typeof value.endsPattern === 'string' ? { regexp: value.endsPattern } : value.endsPattern
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export enum ProblemLocationKind {
|
||||
File,
|
||||
Location
|
||||
}
|
||||
|
||||
export namespace ProblemLocationKind {
|
||||
export function fromString(value: string): ProblemLocationKind | undefined {
|
||||
value = value.toLowerCase();
|
||||
if (value === 'file') {
|
||||
return ProblemLocationKind.File;
|
||||
} else if (value === 'location') {
|
||||
return ProblemLocationKind.Location;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProblemMatcher {
|
||||
deprecated?: boolean;
|
||||
|
||||
owner: string;
|
||||
source?: string;
|
||||
applyTo: ApplyToKind;
|
||||
fileLocation: FileLocationKind;
|
||||
filePrefix?: string;
|
||||
pattern: ProblemPattern | ProblemPattern[];
|
||||
severity?: Severity;
|
||||
watching?: WatchingMatcher;
|
||||
uriProvider?: (path: string) => URI;
|
||||
}
|
||||
|
||||
export interface NamedProblemMatcher extends ProblemMatcher {
|
||||
name: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export namespace ProblemMatcher {
|
||||
export function isWatchModeWatcher(matcher: ProblemMatcher): boolean {
|
||||
return !!matcher.watching;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProblemPattern {
|
||||
name?: string;
|
||||
|
||||
regexp: string;
|
||||
|
||||
kind?: ProblemLocationKind;
|
||||
file?: number;
|
||||
message?: number;
|
||||
location?: number;
|
||||
line?: number;
|
||||
character?: number;
|
||||
endLine?: number;
|
||||
endCharacter?: number;
|
||||
code?: number;
|
||||
severity?: number;
|
||||
loop?: boolean;
|
||||
}
|
||||
|
||||
export interface NamedProblemPattern extends ProblemPattern {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export namespace ProblemPattern {
|
||||
export function fromProblemPatternContribution(value: ProblemPatternContribution): ProblemPattern {
|
||||
return {
|
||||
name: value.name,
|
||||
regexp: value.regexp,
|
||||
kind: value.kind ? ProblemLocationKind.fromString(value.kind) : undefined,
|
||||
file: value.file,
|
||||
message: value.message,
|
||||
location: value.location,
|
||||
line: value.line,
|
||||
character: value.column || value.character,
|
||||
endLine: value.endLine,
|
||||
endCharacter: value.endColumn || value.endCharacter,
|
||||
code: value.code,
|
||||
severity: value.severity,
|
||||
loop: value.loop
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProblemMatch {
|
||||
resource?: URI;
|
||||
description: ProblemMatcher;
|
||||
}
|
||||
|
||||
export interface ProblemMatchData extends ProblemMatch {
|
||||
marker: Diagnostic;
|
||||
}
|
||||
export namespace ProblemMatchData {
|
||||
export function is(data: ProblemMatch): data is ProblemMatchData {
|
||||
return 'marker' in data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface WatchingMatcherContribution {
|
||||
// If set to true the background monitor is in active mode when the task starts.
|
||||
// This is equals of issuing a line that matches the beginPattern
|
||||
activeOnStart?: boolean;
|
||||
beginsPattern: string | WatchingPattern;
|
||||
endsPattern: string | WatchingPattern;
|
||||
}
|
||||
|
||||
export interface ProblemMatcherContribution {
|
||||
base?: string;
|
||||
name?: string;
|
||||
label: string;
|
||||
deprecated?: boolean;
|
||||
|
||||
owner: string;
|
||||
source?: string;
|
||||
applyTo?: string;
|
||||
fileLocation?: 'absolute' | 'relative' | string[];
|
||||
pattern: string | ProblemPatternContribution | ProblemPatternContribution[];
|
||||
severity?: string;
|
||||
watching?: WatchingMatcherContribution; // deprecated. Use `background`.
|
||||
background?: WatchingMatcherContribution;
|
||||
}
|
||||
|
||||
export interface ProblemPatternContribution {
|
||||
name?: string;
|
||||
regexp: string;
|
||||
|
||||
kind?: string;
|
||||
file?: number;
|
||||
message?: number;
|
||||
location?: number;
|
||||
line?: number;
|
||||
character?: number;
|
||||
column?: number;
|
||||
endLine?: number;
|
||||
endCharacter?: number;
|
||||
endColumn?: number;
|
||||
code?: number;
|
||||
severity?: number;
|
||||
loop?: boolean;
|
||||
}
|
||||
97
packages/task/src/common/process/task-protocol.ts
Normal file
97
packages/task/src/common/process/task-protocol.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TaskConfiguration, TaskInfo } from '../task-protocol';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
|
||||
export type ProcessType = 'shell' | 'process';
|
||||
|
||||
export interface CommandOptions {
|
||||
/**
|
||||
* The 'current working directory' the task will run in. Can be a uri-as-string
|
||||
* or plain string path. If the cwd is meant to be somewhere under the workspace,
|
||||
* one can use the variable `${workspaceFolder}`, which will be replaced by its path,
|
||||
* at runtime. If not specified, defaults to the workspace root.
|
||||
* ex: cwd: '${workspaceFolder}/foo'
|
||||
*/
|
||||
cwd?: string;
|
||||
|
||||
/**
|
||||
* The environment of the executed program or shell. If omitted the parent process' environment is used.
|
||||
*/
|
||||
env?: { [key: string]: string | undefined; };
|
||||
|
||||
/**
|
||||
* Configuration of the shell when task type is `shell`
|
||||
*/
|
||||
shell?: {
|
||||
/**
|
||||
* The shell to use.
|
||||
*/
|
||||
executable: string;
|
||||
|
||||
/**
|
||||
* The arguments to be passed to the shell executable to run in command mode
|
||||
* (e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe).
|
||||
*/
|
||||
args?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface CommandProperties {
|
||||
readonly command?: string;
|
||||
readonly args?: string[];
|
||||
readonly options?: CommandOptions;
|
||||
}
|
||||
|
||||
/** Configuration of a Task that may be run as a process or a command inside a shell. */
|
||||
export interface ProcessTaskConfiguration extends TaskConfiguration, CommandProperties {
|
||||
readonly type: ProcessType;
|
||||
|
||||
/**
|
||||
* Windows specific task configuration
|
||||
*/
|
||||
readonly windows?: CommandProperties;
|
||||
|
||||
/**
|
||||
* macOS specific task configuration
|
||||
*/
|
||||
readonly osx?: CommandProperties;
|
||||
|
||||
/**
|
||||
* Linux specific task configuration
|
||||
*/
|
||||
readonly linux?: CommandProperties;
|
||||
}
|
||||
|
||||
export interface ProcessTaskInfo extends TaskInfo {
|
||||
/** process id. Defined if task is run as a process */
|
||||
readonly processId?: number;
|
||||
/** process task command */
|
||||
readonly command?: string;
|
||||
}
|
||||
export namespace ProcessTaskInfo {
|
||||
export function is(info: TaskInfo): info is ProcessTaskInfo {
|
||||
return info['processId'] !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ProcessTaskError {
|
||||
export const CouldNotRun = ApplicationError.declare(1, (code: string) => ({
|
||||
message: `Error starting process (${code})`,
|
||||
data: { code }
|
||||
}));
|
||||
}
|
||||
27
packages/task/src/common/task-common-module.ts
Normal file
27
packages/task/src/common/task-common-module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { TaskWatcher } from './task-watcher';
|
||||
|
||||
/**
|
||||
* Create the bindings common to node and browser.
|
||||
*
|
||||
* @param bind The bind function from inversify.
|
||||
*/
|
||||
export function createCommonBindings(bind: interfaces.Bind): void {
|
||||
bind(TaskWatcher).toSelf().inSingletonScope();
|
||||
}
|
||||
41
packages/task/src/common/task-preferences.ts
Normal file
41
packages/task/src/common/task-preferences.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { PreferenceConfiguration } from '@theia/core/lib/common/preferences/preference-configurations';
|
||||
import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
|
||||
import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope';
|
||||
|
||||
export const taskSchemaId = 'vscode://schemas/tasks';
|
||||
|
||||
export const taskPreferencesSchema: PreferenceSchema = {
|
||||
scope: PreferenceScope.Folder,
|
||||
properties: {
|
||||
tasks: {
|
||||
$ref: taskSchemaId,
|
||||
description: 'Task definition file',
|
||||
default: {
|
||||
version: '2.0.0',
|
||||
tasks: []
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function bindTaskPreferences(bind: interfaces.Bind): void {
|
||||
bind(PreferenceContribution).toConstantValue({ schema: taskPreferencesSchema });
|
||||
bind(PreferenceConfiguration).toConstantValue({ name: 'tasks' });
|
||||
}
|
||||
318
packages/task/src/common/task-protocol.ts
Normal file
318
packages/task/src/common/task-protocol.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Event } from '@theia/core';
|
||||
import { RpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
|
||||
import { ProblemMatcher, ProblemMatch, WatchingMatcherContribution, ProblemMatcherContribution, ProblemPatternContribution } from './problem-matcher-protocol';
|
||||
export { WatchingMatcherContribution, ProblemMatcherContribution, ProblemPatternContribution };
|
||||
|
||||
export const taskPath = '/services/task';
|
||||
|
||||
export const TaskServer = Symbol('TaskServer');
|
||||
export const TaskClient = Symbol('TaskClient');
|
||||
export enum DependsOrder {
|
||||
Sequence = 'sequence',
|
||||
Parallel = 'parallel',
|
||||
}
|
||||
|
||||
export enum RevealKind {
|
||||
Always = 'always',
|
||||
Silent = 'silent',
|
||||
Never = 'never'
|
||||
}
|
||||
|
||||
export enum PanelKind {
|
||||
Shared = 'shared',
|
||||
Dedicated = 'dedicated',
|
||||
New = 'new'
|
||||
}
|
||||
|
||||
export interface TaskOutputPresentation {
|
||||
echo?: boolean;
|
||||
focus?: boolean;
|
||||
reveal?: RevealKind;
|
||||
panel?: PanelKind;
|
||||
showReuseMessage?: boolean;
|
||||
clear?: boolean;
|
||||
close?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[name: string]: any;
|
||||
}
|
||||
export namespace TaskOutputPresentation {
|
||||
export function getDefault(): TaskOutputPresentation {
|
||||
return {
|
||||
echo: true,
|
||||
reveal: RevealKind.Always,
|
||||
focus: false,
|
||||
panel: PanelKind.Shared,
|
||||
showReuseMessage: true,
|
||||
clear: false,
|
||||
close: false
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function fromJson(task: any): TaskOutputPresentation {
|
||||
let outputPresentation = getDefault();
|
||||
if (task && task.presentation) {
|
||||
if (task.presentation.reveal) {
|
||||
let reveal = RevealKind.Always;
|
||||
if (task.presentation.reveal === 'silent') {
|
||||
reveal = RevealKind.Silent;
|
||||
} else if (task.presentation.reveal === 'never') {
|
||||
reveal = RevealKind.Never;
|
||||
}
|
||||
outputPresentation = { ...outputPresentation, reveal };
|
||||
}
|
||||
if (task.presentation.panel) {
|
||||
let panel = PanelKind.Shared;
|
||||
if (task.presentation.panel === 'dedicated') {
|
||||
panel = PanelKind.Dedicated;
|
||||
} else if (task.presentation.panel === 'new') {
|
||||
panel = PanelKind.New;
|
||||
}
|
||||
outputPresentation = { ...outputPresentation, panel };
|
||||
}
|
||||
outputPresentation = {
|
||||
...outputPresentation,
|
||||
echo: task.presentation.echo === undefined || task.presentation.echo,
|
||||
focus: shouldSetFocusToTerminal(task),
|
||||
showReuseMessage: shouldShowReuseMessage(task),
|
||||
clear: shouldClearTerminalBeforeRun(task),
|
||||
close: shouldCloseTerminalOnFinish(task)
|
||||
};
|
||||
}
|
||||
return outputPresentation;
|
||||
}
|
||||
|
||||
export function shouldAlwaysRevealTerminal(task: TaskCustomization): boolean {
|
||||
return !task.presentation || task.presentation.reveal === undefined || task.presentation.reveal === RevealKind.Always;
|
||||
}
|
||||
|
||||
export function shouldSetFocusToTerminal(task: TaskCustomization): boolean {
|
||||
return !!task.presentation && !!task.presentation.focus;
|
||||
}
|
||||
|
||||
export function shouldClearTerminalBeforeRun(task: TaskCustomization): boolean {
|
||||
return !!task.presentation && !!task.presentation.clear;
|
||||
}
|
||||
|
||||
export function shouldCloseTerminalOnFinish(task: TaskCustomization): boolean {
|
||||
return !!task.presentation && !!task.presentation.close;
|
||||
}
|
||||
|
||||
export function shouldShowReuseMessage(task: TaskCustomization): boolean {
|
||||
return !task.presentation || task.presentation.showReuseMessage === undefined || !!task.presentation.showReuseMessage;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TaskCustomization {
|
||||
type: string;
|
||||
group?: 'build' | 'test' | 'rebuild' | 'clean' | 'none' | { kind: 'build' | 'test' | 'rebuild' | 'clean', isDefault: boolean };
|
||||
problemMatcher?: string | ProblemMatcherContribution | (string | ProblemMatcherContribution)[];
|
||||
presentation?: TaskOutputPresentation;
|
||||
detail?: string;
|
||||
|
||||
/** Whether the task is a background task or not. */
|
||||
isBackground?: boolean;
|
||||
|
||||
/** The other tasks the task depend on. */
|
||||
dependsOn?: string | TaskIdentifier | Array<string | TaskIdentifier>;
|
||||
|
||||
/** The order the dependsOn tasks should be executed in. */
|
||||
dependsOrder?: DependsOrder;
|
||||
|
||||
runOptions?: RunOptions;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[name: string]: any;
|
||||
}
|
||||
export namespace TaskCustomization {
|
||||
export function isBuildTask(task: TaskCustomization): boolean {
|
||||
return task.group === 'build' || typeof task.group === 'object' && task.group.kind === 'build';
|
||||
}
|
||||
|
||||
export function isDefaultBuildTask(task: TaskCustomization): boolean {
|
||||
return isDefaultTask(task) && isBuildTask(task);
|
||||
}
|
||||
|
||||
export function isDefaultTask(task: TaskCustomization): boolean {
|
||||
return typeof task.group === 'object' && task.group.isDefault;
|
||||
}
|
||||
|
||||
export function isTestTask(task: TaskCustomization): boolean {
|
||||
return task.group === 'test' || typeof task.group === 'object' && task.group.kind === 'test';
|
||||
}
|
||||
|
||||
export function isDefaultTestTask(task: TaskCustomization): boolean {
|
||||
return isDefaultTask(task) && isTestTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
export enum TaskScope {
|
||||
Global = 1,
|
||||
Workspace = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* The task configuration scopes.
|
||||
* - `string` represents the associated workspace folder uri.
|
||||
*/
|
||||
export type TaskConfigurationScope = string | TaskScope.Workspace | TaskScope.Global;
|
||||
|
||||
export interface TaskConfiguration extends TaskCustomization {
|
||||
/** A label that uniquely identifies a task configuration per source */
|
||||
readonly label: string;
|
||||
readonly _scope: TaskConfigurationScope;
|
||||
readonly executionType?: 'shell' | 'process' | 'customExecution';
|
||||
}
|
||||
|
||||
export interface ContributedTaskConfiguration extends TaskConfiguration {
|
||||
/**
|
||||
* Source of the task configuration.
|
||||
* For a configured task, it is the name of the root folder, while for a provided task, it is the name of the provider.
|
||||
* This field is not supposed to be used in `tasks.json`
|
||||
*/
|
||||
readonly _source: string;
|
||||
}
|
||||
|
||||
/** A task identifier */
|
||||
export interface TaskIdentifier {
|
||||
type: string;
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
/** Runtime information about Task. */
|
||||
export interface TaskInfo {
|
||||
/** internal unique task id */
|
||||
readonly taskId: number,
|
||||
/** terminal id. Defined if task is run as a terminal process */
|
||||
readonly terminalId?: number,
|
||||
/** context that was passed as part of task creation, if any */
|
||||
readonly ctx?: string,
|
||||
/** task config used for launching a task */
|
||||
readonly config: TaskConfiguration,
|
||||
/** Additional properties specific for a particular Task Runner. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly [key: string]: any;
|
||||
}
|
||||
|
||||
export interface TaskServer extends RpcServer<TaskClient> {
|
||||
/** Run a task. Optionally pass a context. */
|
||||
run(task: TaskConfiguration, ctx?: string, option?: RunTaskOption): Promise<TaskInfo>;
|
||||
/** Kill a task, by id. */
|
||||
kill(taskId: number): Promise<void>;
|
||||
/**
|
||||
* Returns a list of currently running tasks. If a context is provided,
|
||||
* only the tasks started in that context will be provided. Using an
|
||||
* undefined context matches all tasks, no matter the creation context.
|
||||
*/
|
||||
getTasks(ctx?: string): Promise<TaskInfo[]>
|
||||
|
||||
/** removes the client that has disconnected */
|
||||
disconnectClient(client: TaskClient): void;
|
||||
|
||||
/** Returns the list of default and registered task runners */
|
||||
getRegisteredTaskTypes(): Promise<string[]>
|
||||
|
||||
/** plugin callback task complete */
|
||||
customExecutionComplete(id: number, exitCode: number | undefined): Promise<void>
|
||||
}
|
||||
|
||||
export interface TaskCustomizationData {
|
||||
type: string;
|
||||
problemMatcher?: ProblemMatcher[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
export interface RunTaskOption {
|
||||
customization?: TaskCustomizationData;
|
||||
}
|
||||
|
||||
export interface RunOptions {
|
||||
reevaluateOnRerun?: boolean;
|
||||
}
|
||||
|
||||
/** Event sent when a task has concluded its execution */
|
||||
export interface TaskExitedEvent {
|
||||
readonly taskId: number;
|
||||
readonly ctx?: string;
|
||||
|
||||
// Exactly one of code and signal will be set.
|
||||
readonly code?: number;
|
||||
readonly signal?: string;
|
||||
|
||||
readonly config?: TaskConfiguration;
|
||||
|
||||
readonly terminalId?: number;
|
||||
readonly processId?: number;
|
||||
}
|
||||
|
||||
export interface TaskOutputEvent {
|
||||
readonly taskId: number;
|
||||
readonly ctx?: string;
|
||||
readonly line: string;
|
||||
}
|
||||
|
||||
export interface TaskOutputProcessedEvent {
|
||||
readonly taskId: number;
|
||||
readonly config: TaskConfiguration;
|
||||
readonly ctx?: string;
|
||||
readonly problems?: ProblemMatch[];
|
||||
}
|
||||
|
||||
export interface BackgroundTaskEndedEvent {
|
||||
readonly taskId: number;
|
||||
readonly ctx?: string;
|
||||
}
|
||||
|
||||
export interface TaskClient {
|
||||
onTaskExit(event: TaskExitedEvent): void;
|
||||
onTaskCreated(event: TaskInfo): void;
|
||||
onDidStartTaskProcess(event: TaskInfo): void;
|
||||
onDidEndTaskProcess(event: TaskExitedEvent): void;
|
||||
onDidProcessTaskOutput(event: TaskOutputProcessedEvent): void;
|
||||
onBackgroundTaskEnded(event: BackgroundTaskEndedEvent): void;
|
||||
}
|
||||
|
||||
export interface TaskDefinition {
|
||||
taskType: string;
|
||||
source: string;
|
||||
properties: {
|
||||
/**
|
||||
* Should be treated as an empty array if omitted.
|
||||
* https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.5.3
|
||||
*/
|
||||
required?: string[];
|
||||
all: string[];
|
||||
schema: IJSONSchema;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ManagedTask {
|
||||
id: number;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface ManagedTaskManager<T extends ManagedTask> {
|
||||
onDelete: Event<number>;
|
||||
register(task: T, context?: string): number;
|
||||
get(id: number): T | undefined;
|
||||
getTasks(context?: string): T[] | undefined;
|
||||
delete(task: T): void;
|
||||
}
|
||||
43
packages/task/src/common/task-util.ts
Normal file
43
packages/task/src/common/task-util.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 EclipseSource 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* Converts the given standard name to a variable name starting with '$' if not already present.
|
||||
*
|
||||
* Variable names are used, for instance, to reference problem matchers, within task configurations.
|
||||
*
|
||||
* @param name standard name
|
||||
* @returns variable name with leading '$' if not already present.
|
||||
*
|
||||
* @see {@link fromVariableName} for the reverse conversion.
|
||||
*/
|
||||
export function asVariableName(name: string): string {
|
||||
return name.startsWith('$') ? name : `$${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given variable name to a standard name, effectively removing a leading '$' if present.
|
||||
*
|
||||
* Standard names are used, for instance, in registries to store variable objects
|
||||
*
|
||||
* @param name variable name
|
||||
* @returns variable name without leading '$' if present.
|
||||
*
|
||||
* @see {@link asVariableName} for the reverse conversion.
|
||||
*/
|
||||
export function fromVariableName(name: string): string {
|
||||
return name.startsWith('$') ? name.slice(1) : name;
|
||||
}
|
||||
78
packages/task/src/common/task-watcher.ts
Normal file
78
packages/task/src/common/task-watcher.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { TaskClient, TaskExitedEvent, TaskInfo, TaskOutputProcessedEvent, BackgroundTaskEndedEvent } from './task-protocol';
|
||||
|
||||
@injectable()
|
||||
export class TaskWatcher {
|
||||
|
||||
getTaskClient(): TaskClient {
|
||||
const newTaskEmitter = this.onTaskCreatedEmitter;
|
||||
const exitEmitter = this.onTaskExitEmitter;
|
||||
const taskProcessStartedEmitter = this.onDidStartTaskProcessEmitter;
|
||||
const taskProcessEndedEmitter = this.onDidEndTaskProcessEmitter;
|
||||
const outputProcessedEmitter = this.onOutputProcessedEmitter;
|
||||
const backgroundTaskEndedEmitter = this.onBackgroundTaskEndedEmitter;
|
||||
return {
|
||||
onTaskCreated(event: TaskInfo): void {
|
||||
newTaskEmitter.fire(event);
|
||||
},
|
||||
onTaskExit(event: TaskExitedEvent): void {
|
||||
exitEmitter.fire(event);
|
||||
},
|
||||
onDidStartTaskProcess(event: TaskInfo): void {
|
||||
taskProcessStartedEmitter.fire(event);
|
||||
},
|
||||
onDidEndTaskProcess(event: TaskExitedEvent): void {
|
||||
taskProcessEndedEmitter.fire(event);
|
||||
},
|
||||
onDidProcessTaskOutput(event: TaskOutputProcessedEvent): void {
|
||||
outputProcessedEmitter.fire(event);
|
||||
},
|
||||
onBackgroundTaskEnded(event: BackgroundTaskEndedEvent): void {
|
||||
backgroundTaskEndedEmitter.fire(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected onTaskCreatedEmitter = new Emitter<TaskInfo>();
|
||||
protected onTaskExitEmitter = new Emitter<TaskExitedEvent>();
|
||||
protected onDidStartTaskProcessEmitter = new Emitter<TaskInfo>();
|
||||
protected onDidEndTaskProcessEmitter = new Emitter<TaskExitedEvent>();
|
||||
protected onOutputProcessedEmitter = new Emitter<TaskOutputProcessedEvent>();
|
||||
protected onBackgroundTaskEndedEmitter = new Emitter<BackgroundTaskEndedEvent>();
|
||||
|
||||
get onTaskCreated(): Event<TaskInfo> {
|
||||
return this.onTaskCreatedEmitter.event;
|
||||
}
|
||||
get onTaskExit(): Event<TaskExitedEvent> {
|
||||
return this.onTaskExitEmitter.event;
|
||||
}
|
||||
get onDidStartTaskProcess(): Event<TaskInfo> {
|
||||
return this.onDidStartTaskProcessEmitter.event;
|
||||
}
|
||||
get onDidEndTaskProcess(): Event<TaskExitedEvent> {
|
||||
return this.onDidEndTaskProcessEmitter.event;
|
||||
}
|
||||
get onOutputProcessed(): Event<TaskOutputProcessedEvent> {
|
||||
return this.onOutputProcessedEmitter.event;
|
||||
}
|
||||
get onBackgroundTaskEnded(): Event<BackgroundTaskEndedEvent> {
|
||||
return this.onBackgroundTaskEndedEmitter.event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 ByteDance 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import { CustomTask, TaskFactory, TaskCustomOptions } from './custom-task';
|
||||
import { CustomTaskRunner } from './custom-task-runner';
|
||||
import { CustomTaskRunnerContribution } from './custom-task-runner-contribution';
|
||||
import { TaskRunnerContribution } from '../task-runner';
|
||||
|
||||
export function bindCustomTaskRunnerModule(bind: interfaces.Bind): void {
|
||||
|
||||
bind(CustomTask).toSelf().inTransientScope();
|
||||
bind(TaskFactory).toFactory(ctx =>
|
||||
(options: TaskCustomOptions) => {
|
||||
const child = new Container({ defaultScope: 'Singleton' });
|
||||
child.parent = ctx.container;
|
||||
child.bind(TaskCustomOptions).toConstantValue(options);
|
||||
return child.get(CustomTask);
|
||||
}
|
||||
);
|
||||
bind(CustomTaskRunner).toSelf().inSingletonScope();
|
||||
bind(CustomTaskRunnerContribution).toSelf().inSingletonScope();
|
||||
bind(TaskRunnerContribution).toService(CustomTaskRunnerContribution);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 ByteDance 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { CustomTaskRunner } from './custom-task-runner';
|
||||
import { TaskRunnerContribution, TaskRunnerRegistry } from '../task-runner';
|
||||
|
||||
@injectable()
|
||||
export class CustomTaskRunnerContribution implements TaskRunnerContribution {
|
||||
|
||||
@inject(CustomTaskRunner)
|
||||
protected readonly customTaskRunner: CustomTaskRunner;
|
||||
|
||||
registerRunner(runners: TaskRunnerRegistry): void {
|
||||
runners.registerRunner('customExecution', this.customTaskRunner);
|
||||
}
|
||||
}
|
||||
60
packages/task/src/node/custom/custom-task-runner.ts
Normal file
60
packages/task/src/node/custom/custom-task-runner.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 ByteDance 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TaskConfiguration } from '../../common';
|
||||
import { Task } from '../task';
|
||||
import { TaskRunner } from '../task-runner';
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { TaskFactory } from './custom-task';
|
||||
import {
|
||||
TerminalProcessFactory,
|
||||
Process,
|
||||
TerminalProcessOptions,
|
||||
} from '@theia/process/lib/node';
|
||||
|
||||
/**
|
||||
* Task runner that runs a task as a pseudoterminal open.
|
||||
*/
|
||||
@injectable()
|
||||
export class CustomTaskRunner implements TaskRunner {
|
||||
|
||||
@inject(ILogger) @named('task')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(TerminalProcessFactory)
|
||||
protected readonly terminalProcessFactory: TerminalProcessFactory;
|
||||
|
||||
@inject(TaskFactory)
|
||||
protected readonly taskFactory: TaskFactory;
|
||||
|
||||
async run(taskConfig: TaskConfiguration, ctx?: string): Promise<Task> {
|
||||
try {
|
||||
const terminalProcessOptions = { isPseudo: true } as TerminalProcessOptions;
|
||||
const terminal: Process = this.terminalProcessFactory(terminalProcessOptions);
|
||||
|
||||
return this.taskFactory({
|
||||
context: ctx,
|
||||
config: taskConfig,
|
||||
label: taskConfig.label,
|
||||
process: terminal,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error occurred while creating task: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
packages/task/src/node/custom/custom-task.ts
Normal file
73
packages/task/src/node/custom/custom-task.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 ByteDance 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { ILogger, MaybePromise } from '@theia/core/lib/common/';
|
||||
import { Task, TaskOptions } from '../task';
|
||||
import { TaskManager } from '../task-manager';
|
||||
import { TaskInfo } from '../../common/task-protocol';
|
||||
import { Process } from '@theia/process/lib/node';
|
||||
|
||||
export const TaskCustomOptions = Symbol('TaskCustomOptions');
|
||||
export interface TaskCustomOptions extends TaskOptions {
|
||||
process: Process
|
||||
}
|
||||
|
||||
export const TaskFactory = Symbol('TaskFactory');
|
||||
export type TaskFactory = (options: TaskCustomOptions) => CustomTask;
|
||||
|
||||
/** Represents a Task launched as a fake process by `CustomTaskRunner`. */
|
||||
@injectable()
|
||||
export class CustomTask extends Task {
|
||||
|
||||
constructor(
|
||||
@inject(TaskManager) taskManager: TaskManager,
|
||||
@inject(ILogger) @named('task') logger: ILogger,
|
||||
@inject(TaskCustomOptions) protected override readonly options: TaskCustomOptions
|
||||
) {
|
||||
super(taskManager, logger, options);
|
||||
this.logger.info(`Created new custom task, id: ${this.id}, context: ${this.context}`);
|
||||
}
|
||||
|
||||
kill(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getRuntimeInfo(): MaybePromise<TaskInfo> {
|
||||
return {
|
||||
taskId: this.id,
|
||||
ctx: this.context,
|
||||
config: this.options.config,
|
||||
terminalId: this.process.id,
|
||||
processId: this.process.id
|
||||
};
|
||||
}
|
||||
|
||||
public callbackTaskComplete(exitCode: number | undefined): MaybePromise<void> {
|
||||
this.fireTaskExited({
|
||||
taskId: this.taskId,
|
||||
ctx: this.context,
|
||||
config: this.options.config,
|
||||
terminalId: this.process.id,
|
||||
processId: this.process.id,
|
||||
code: exitCode || 0
|
||||
});
|
||||
}
|
||||
|
||||
get process(): Process {
|
||||
return this.options.process;
|
||||
}
|
||||
}
|
||||
19
packages/task/src/node/index.ts
Normal file
19
packages/task/src/node/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './task';
|
||||
export * from './task-runner';
|
||||
export * from './task-manager';
|
||||
@@ -0,0 +1,37 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces, Container } from '@theia/core/shared/inversify';
|
||||
import { ProcessTask, TaskFactory, TaskProcessOptions } from './process-task';
|
||||
import { ProcessTaskRunner } from './process-task-runner';
|
||||
import { ProcessTaskRunnerContribution } from './process-task-runner-contribution';
|
||||
import { TaskRunnerContribution } from '../task-runner';
|
||||
|
||||
export function bindProcessTaskRunnerModule(bind: interfaces.Bind): void {
|
||||
|
||||
bind(ProcessTask).toSelf().inTransientScope();
|
||||
bind(TaskFactory).toFactory(ctx =>
|
||||
(options: TaskProcessOptions) => {
|
||||
const child = new Container({ defaultScope: 'Singleton' });
|
||||
child.parent = ctx.container;
|
||||
child.bind(TaskProcessOptions).toConstantValue(options);
|
||||
return child.get(ProcessTask);
|
||||
}
|
||||
);
|
||||
bind(ProcessTaskRunner).toSelf().inSingletonScope();
|
||||
bind(ProcessTaskRunnerContribution).toSelf().inSingletonScope();
|
||||
bind(TaskRunnerContribution).toService(ProcessTaskRunnerContribution);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { ProcessTaskRunner } from './process-task-runner';
|
||||
import { TaskRunnerContribution, TaskRunnerRegistry } from '../task-runner';
|
||||
|
||||
@injectable()
|
||||
export class ProcessTaskRunnerContribution implements TaskRunnerContribution {
|
||||
|
||||
@inject(ProcessTaskRunner)
|
||||
protected readonly processTaskRunner: ProcessTaskRunner;
|
||||
|
||||
registerRunner(runners: TaskRunnerRegistry): void {
|
||||
runners.registerRunner('process', this.processTaskRunner);
|
||||
runners.registerRunner('shell', this.processTaskRunner);
|
||||
}
|
||||
}
|
||||
371
packages/task/src/node/process/process-task-runner.ts
Normal file
371
packages/task/src/node/process/process-task-runner.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017-2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { deepClone, isWindows, isOSX, ILogger } from '@theia/core';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import {
|
||||
RawProcessFactory,
|
||||
ProcessErrorEvent,
|
||||
Process,
|
||||
TerminalProcessOptions,
|
||||
TaskTerminalProcessFactory,
|
||||
} from '@theia/process/lib/node';
|
||||
import {
|
||||
ShellQuotedString, ShellQuotingFunctions, BashQuotingFunctions, CmdQuotingFunctions, PowershellQuotingFunctions, createShellCommandLine, ShellQuoting,
|
||||
} from '@theia/process/lib/common/shell-quoting';
|
||||
import { TaskFactory } from './process-task';
|
||||
import { TaskRunner } from '../task-runner-protocol';
|
||||
import { Task } from '../task';
|
||||
import { TaskConfiguration } from '../../common/task-protocol';
|
||||
import { ProcessTaskError, CommandOptions } from '../../common/process/task-protocol';
|
||||
import * as fs from 'fs';
|
||||
import { ShellProcess } from '@theia/terminal/lib/node/shell-process';
|
||||
|
||||
export interface OsSpecificCommand {
|
||||
command: string,
|
||||
args: Array<string | ShellQuotedString> | undefined,
|
||||
options: CommandOptions
|
||||
}
|
||||
|
||||
export interface ShellSpecificOptions {
|
||||
/** Arguments passed to the shell, aka `command` here. */
|
||||
execArgs: string[];
|
||||
/** Pack of functions used to escape the `subCommand` and `subArgs` to run in the shell. */
|
||||
quotingFunctions?: ShellQuotingFunctions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task runner that runs a task as a process or a command inside a shell.
|
||||
*/
|
||||
@injectable()
|
||||
export class ProcessTaskRunner implements TaskRunner {
|
||||
|
||||
@inject(ILogger) @named('task')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(RawProcessFactory)
|
||||
protected readonly rawProcessFactory: RawProcessFactory;
|
||||
|
||||
@inject(TaskTerminalProcessFactory)
|
||||
protected readonly taskTerminalProcessFactory: TaskTerminalProcessFactory;
|
||||
|
||||
@inject(TaskFactory)
|
||||
protected readonly taskFactory: TaskFactory;
|
||||
|
||||
/**
|
||||
* Runs a task from the given task configuration.
|
||||
* @param taskConfig task configuration to run a task from. The provided task configuration must have a shape of `CommandProperties`.
|
||||
*/
|
||||
async run(taskConfig: TaskConfiguration, ctx?: string): Promise<Task> {
|
||||
if (!taskConfig.command) {
|
||||
throw new Error("Process task config must have 'command' property specified");
|
||||
}
|
||||
try {
|
||||
// Always spawn a task in a pty, the only difference between shell/process tasks is the
|
||||
// way the command is passed:
|
||||
// - process: directly look for an executable and pass a specific set of arguments/options.
|
||||
// - shell: defer the spawning to a shell that will evaluate a command line with our executable.
|
||||
const terminalProcessOptions = this.getResolvedCommand(taskConfig);
|
||||
const terminal: Process = this.taskTerminalProcessFactory(terminalProcessOptions);
|
||||
|
||||
// Wait for the confirmation that the process is successfully started, or has failed to start.
|
||||
await new Promise((resolve, reject) => {
|
||||
terminal.onStart(resolve);
|
||||
terminal.onError((error: ProcessErrorEvent) => {
|
||||
reject(ProcessTaskError.CouldNotRun(error.code));
|
||||
});
|
||||
});
|
||||
|
||||
const processType = (taskConfig.executionType || taskConfig.type) as 'process' | 'shell';
|
||||
return this.taskFactory({
|
||||
label: taskConfig.label,
|
||||
process: terminal,
|
||||
processType,
|
||||
context: ctx,
|
||||
config: taskConfig,
|
||||
command: this.getCommand(processType, terminalProcessOptions)
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error occurred while creating task: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected getResolvedCommand(taskConfig: TaskConfiguration): TerminalProcessOptions {
|
||||
const osSpecificCommand = this.getOsSpecificCommand(taskConfig);
|
||||
|
||||
const options = osSpecificCommand.options;
|
||||
|
||||
// Use task's cwd with spawned process and pass node env object to
|
||||
// new process, so e.g. we can re-use the system path
|
||||
if (options) {
|
||||
options.env = {
|
||||
...process.env,
|
||||
...(options.env || {})
|
||||
};
|
||||
}
|
||||
|
||||
/** Executable to actually spawn. */
|
||||
let command: string;
|
||||
/** List of arguments passed to `command`. */
|
||||
let args: string[];
|
||||
|
||||
/**
|
||||
* Only useful on Windows, has to do with how node-pty handles complex commands.
|
||||
* This string should not include the executable, only what comes after it (arguments).
|
||||
*/
|
||||
let commandLine: string | undefined;
|
||||
|
||||
if ((taskConfig.executionType || taskConfig.type) === 'shell') {
|
||||
// When running a shell task, we have to spawn a shell process somehow,
|
||||
// and tell it to run the command the user wants to run inside of it.
|
||||
//
|
||||
// E.g:
|
||||
// - Spawning a process:
|
||||
// spawn(process_exe, [...args])
|
||||
// - Spawning a shell and run a command:
|
||||
// spawn(shell_exe, [shell_exec_cmd_flag, command])
|
||||
//
|
||||
// The fun part is, the `command` to pass as an argument usually has to be
|
||||
// what you would type verbatim inside the shell, so escaping rules apply.
|
||||
//
|
||||
// What's even more funny is that on Windows, node-pty uses a special
|
||||
// mechanism to pass complex escaped arguments, via a string.
|
||||
//
|
||||
// We need to accommodate most shells, so we need to get specific.
|
||||
|
||||
const { shell } = osSpecificCommand.options;
|
||||
|
||||
command = shell?.executable || ShellProcess.getShellExecutablePath();
|
||||
const { execArgs, quotingFunctions } = this.getShellSpecificOptions(command);
|
||||
|
||||
// Allow overriding shell options from task configuration.
|
||||
args = shell?.args ? [...shell.args] : [...execArgs];
|
||||
|
||||
// Check if an argument list is defined or not. Empty is ok.
|
||||
/** Shell command to run: */
|
||||
const shellCommand = this.buildShellCommand(osSpecificCommand, quotingFunctions);
|
||||
|
||||
if (isWindows && /cmd(.exe)?$/.test(command)) {
|
||||
// Let's take the following command, including an argument containing whitespace:
|
||||
// cmd> node -p process.argv 1 2 " 3"
|
||||
//
|
||||
// We would expect the following output:
|
||||
// json> [ '...\\node.exe', '1', '2', ' 3' ]
|
||||
//
|
||||
// Let's run this command through `cmd.exe` using `child_process`:
|
||||
// js> void childprocess.spawn('cmd.exe', ['/s', '/c', 'node -p process.argv 1 2 " 3"']).stderr.on('data', console.log)
|
||||
//
|
||||
// We get the correct output, but when using node-pty:
|
||||
// js> void nodepty.spawn('cmd.exe', ['/s', '/c', 'node -p process.argv 1 2 " 3"']).on('data', console.log)
|
||||
//
|
||||
// Then the output looks like:
|
||||
// json> [ '...\\node.exe', '1', '2', '"', '3"' ]
|
||||
//
|
||||
// To fix that, we need to use a special node-pty feature and pass arguments as one string:
|
||||
// js> nodepty.spawn('cmd.exe', '/s /c "node -p process.argv 1 2 " 3""')
|
||||
//
|
||||
// Note the extra quotes that need to be added around the whole command.
|
||||
commandLine = [...args, `"${shellCommand}"`].join(' ');
|
||||
}
|
||||
|
||||
args.push(shellCommand);
|
||||
} else {
|
||||
// When running process tasks, `command` is the executable to run,
|
||||
// and `args` are the arguments we want to pass to it.
|
||||
command = osSpecificCommand.command;
|
||||
if (Array.isArray(osSpecificCommand.args)) {
|
||||
// Process task doesn't handle quotation: Normalize arguments from `ShellQuotedString` to raw `string`.
|
||||
args = osSpecificCommand.args.map(arg => typeof arg === 'string' ? arg : arg.value);
|
||||
} else {
|
||||
args = [];
|
||||
}
|
||||
}
|
||||
return { command, args, commandLine, options };
|
||||
}
|
||||
|
||||
protected buildShellCommand(systemSpecificCommand: OsSpecificCommand, quotingFunctions?: ShellQuotingFunctions): string {
|
||||
if (Array.isArray(systemSpecificCommand.args)) {
|
||||
const commandLineElements: Array<string | ShellQuotedString> = [systemSpecificCommand.command, ...systemSpecificCommand.args].map(arg => {
|
||||
// We want to quote arguments only if needed.
|
||||
if (quotingFunctions && typeof arg === 'string' && this.argumentNeedsQuotes(arg, quotingFunctions)) {
|
||||
return {
|
||||
quoting: ShellQuoting.Strong,
|
||||
value: arg,
|
||||
};
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
});
|
||||
return createShellCommandLine(commandLineElements, quotingFunctions);
|
||||
} else {
|
||||
// No arguments are provided, so `command` is actually the full command line to execute.
|
||||
return systemSpecificCommand.command ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
protected getShellSpecificOptions(command: string): ShellSpecificOptions {
|
||||
if (/bash(.exe)?$/.test(command)) {
|
||||
return {
|
||||
quotingFunctions: BashQuotingFunctions,
|
||||
execArgs: ['-c']
|
||||
};
|
||||
} else if (/wsl(.exe)?$/.test(command)) {
|
||||
return {
|
||||
quotingFunctions: BashQuotingFunctions,
|
||||
execArgs: ['-e']
|
||||
};
|
||||
} else if (/cmd(.exe)?$/.test(command)) {
|
||||
return {
|
||||
quotingFunctions: CmdQuotingFunctions,
|
||||
execArgs: ['/S', '/C']
|
||||
};
|
||||
} else if (/(ps|pwsh|powershell)(.exe)?/.test(command)) {
|
||||
return {
|
||||
quotingFunctions: PowershellQuotingFunctions,
|
||||
execArgs: ['-c']
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
quotingFunctions: BashQuotingFunctions,
|
||||
execArgs: ['-l', '-c']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected getOsSpecificCommand(taskConfig: TaskConfiguration): OsSpecificCommand {
|
||||
// on windows, windows-specific options, if available, take precedence
|
||||
if (isWindows && taskConfig.windows !== undefined) {
|
||||
return this.getSystemSpecificCommand(taskConfig, 'windows');
|
||||
} else if (isOSX && taskConfig.osx !== undefined) { // on macOS, mac-specific options, if available, take precedence
|
||||
return this.getSystemSpecificCommand(taskConfig, 'osx');
|
||||
} else if (!isWindows && !isOSX && taskConfig.linux !== undefined) { // on linux, linux-specific options, if available, take precedence
|
||||
return this.getSystemSpecificCommand(taskConfig, 'linux');
|
||||
} else { // system-specific options are unavailable, use the default
|
||||
return this.getSystemSpecificCommand(taskConfig, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
protected getCommand(processType: 'process' | 'shell', terminalProcessOptions: TerminalProcessOptions): string | undefined {
|
||||
if (terminalProcessOptions.args) {
|
||||
if (processType === 'shell') {
|
||||
return terminalProcessOptions.args[terminalProcessOptions.args.length - 1];
|
||||
} else if (processType === 'process') {
|
||||
return `${terminalProcessOptions.command} ${terminalProcessOptions.args.join(' ')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is task specific, to align with VS Code's behavior.
|
||||
*
|
||||
* When parsing arguments, VS Code will try to detect if the user already
|
||||
* tried to quote things.
|
||||
*
|
||||
* See: https://github.com/microsoft/vscode/blob/d363b988e1e58cf49963841c498681cdc6cb55a3/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts#L1101-L1127
|
||||
*
|
||||
* @param value
|
||||
* @param shellQuotingOptions
|
||||
*/
|
||||
protected argumentNeedsQuotes(value: string, shellQuotingOptions: ShellQuotingFunctions): boolean {
|
||||
const { characters } = shellQuotingOptions;
|
||||
const needQuotes = new Set([' ', ...characters.needQuotes || []]);
|
||||
if (!characters) {
|
||||
return false;
|
||||
}
|
||||
if (value.length >= 2) {
|
||||
const first = value[0] === characters.strong ? characters.strong : value[0] === characters.weak ? characters.weak : undefined;
|
||||
if (first === value[value.length - 1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let quote: string | undefined;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
// We found the end quote.
|
||||
const ch = value[i];
|
||||
if (ch === quote) {
|
||||
quote = undefined;
|
||||
} else if (quote !== undefined) {
|
||||
// skip the character. We are quoted.
|
||||
continue;
|
||||
} else if (ch === characters.escape) {
|
||||
// Skip the next character
|
||||
i++;
|
||||
} else if (ch === characters.strong || ch === characters.weak) {
|
||||
quote = ch;
|
||||
} else if (needQuotes.has(ch)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected getSystemSpecificCommand(taskConfig: TaskConfiguration, system: 'windows' | 'linux' | 'osx' | undefined): OsSpecificCommand {
|
||||
// initialize with default values from the `taskConfig`
|
||||
let command: string | undefined = taskConfig.command;
|
||||
let args: Array<string | ShellQuotedString> | undefined = taskConfig.args;
|
||||
let options: CommandOptions = deepClone(taskConfig.options) || {};
|
||||
|
||||
if (system) {
|
||||
if (taskConfig[system].command) {
|
||||
command = taskConfig[system].command;
|
||||
}
|
||||
if (taskConfig[system].args) {
|
||||
args = taskConfig[system].args;
|
||||
}
|
||||
if (taskConfig[system].options) {
|
||||
options = taskConfig[system].options;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.cwd) {
|
||||
options.cwd = this.asFsPath(options.cwd);
|
||||
}
|
||||
|
||||
if (command === undefined) {
|
||||
throw new Error('The `command` field of a task cannot be undefined.');
|
||||
}
|
||||
|
||||
return { command, args, options };
|
||||
}
|
||||
|
||||
protected asFsPath(uriOrPath: string): string {
|
||||
return (uriOrPath.startsWith('file:'))
|
||||
? FileUri.fsPath(uriOrPath)
|
||||
: uriOrPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Remove ProcessTaskRunner.findCommand, introduce process "started" event
|
||||
* Checks for the existence of a file, at the provided path, and make sure that
|
||||
* it's readable and executable.
|
||||
*/
|
||||
protected async executableFileExists(filePath: string): Promise<boolean> {
|
||||
return new Promise<boolean>(async (resolve, reject) => {
|
||||
fs.access(filePath, fs.constants.F_OK | fs.constants.X_OK, err => {
|
||||
resolve(err ? false : true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
30
packages/task/src/node/process/process-task.spec.ts
Normal file
30
packages/task/src/node/process/process-task.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { removeAnsiEscapeCodes } from './process-task';
|
||||
|
||||
describe('removeAnsiEscapeCodes function', () => {
|
||||
it('should remove all end line and color codes', () => {
|
||||
const str1 = ' [2m14:21[22m [33mwarning[39m Missing semicolon [2msemi[22m\r';
|
||||
let res = removeAnsiEscapeCodes(str1);
|
||||
expect(res).to.eq(' 14:21 warning Missing semicolon semi');
|
||||
|
||||
const str2 = '[37;40mnpm[0m [0m[31;40mERR![0m [0m[35mcode[0m ELIFECYCLE\r';
|
||||
res = removeAnsiEscapeCodes(str2);
|
||||
expect(res).to.eq('npm ERR! code ELIFECYCLE');
|
||||
});
|
||||
});
|
||||
144
packages/task/src/node/process/process-task.ts
Normal file
144
packages/task/src/node/process/process-task.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/';
|
||||
import { Process, IProcessExitEvent } from '@theia/process/lib/node';
|
||||
import { Task, TaskOptions } from '../task';
|
||||
import { TaskManager } from '../task-manager';
|
||||
import { ProcessType, ProcessTaskInfo } from '../../common/process/task-protocol';
|
||||
import { TaskExitedEvent } from '../../common/task-protocol';
|
||||
|
||||
// copied from https://github.com/microsoft/vscode/blob/1.79.0/src/vs/base/common/strings.ts#L736
|
||||
const CSI_SEQUENCE = /(:?\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/g;
|
||||
|
||||
// Plus additional markers for custom `\x1b]...\x07` instructions.
|
||||
const CSI_CUSTOM_SEQUENCE = /\x1b\].*?\x07/g;
|
||||
|
||||
export function removeAnsiEscapeCodes(str: string): string {
|
||||
if (str) {
|
||||
str = str.replace(CSI_SEQUENCE, '').replace(CSI_CUSTOM_SEQUENCE, '');
|
||||
}
|
||||
|
||||
return str.trimEnd();
|
||||
}
|
||||
|
||||
export const TaskProcessOptions = Symbol('TaskProcessOptions');
|
||||
export interface TaskProcessOptions extends TaskOptions {
|
||||
process: Process;
|
||||
processType: ProcessType;
|
||||
command?: string;
|
||||
}
|
||||
|
||||
export const TaskFactory = Symbol('TaskFactory');
|
||||
export type TaskFactory = (options: TaskProcessOptions) => ProcessTask;
|
||||
|
||||
/** Represents a Task launched as a process by `ProcessTaskRunner`. */
|
||||
@injectable()
|
||||
export class ProcessTask extends Task {
|
||||
|
||||
protected command: string | undefined;
|
||||
|
||||
constructor(
|
||||
@inject(TaskManager) taskManager: TaskManager,
|
||||
@inject(ILogger) @named('task') logger: ILogger,
|
||||
@inject(TaskProcessOptions) protected override readonly options: TaskProcessOptions
|
||||
) {
|
||||
super(taskManager, logger, options);
|
||||
|
||||
const toDispose = this.process.onClose(async event => {
|
||||
toDispose.dispose();
|
||||
this.fireTaskExited(await this.getTaskExitedEvent(event));
|
||||
});
|
||||
|
||||
// Buffer to accumulate incoming output.
|
||||
let dataBuffer: string = '';
|
||||
this.process.outputStream.on('data', (chunk: string) => {
|
||||
dataBuffer += chunk;
|
||||
|
||||
while (1) {
|
||||
// Check if we have a complete line.
|
||||
const eolIdx = dataBuffer.indexOf('\n');
|
||||
if (eolIdx < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Get and remove the line from the data buffer.
|
||||
const lineBuf = dataBuffer.slice(0, eolIdx);
|
||||
dataBuffer = dataBuffer.slice(eolIdx + 1);
|
||||
const processedLine = removeAnsiEscapeCodes(lineBuf);
|
||||
this.fireOutputLine({
|
||||
taskId: this.taskId,
|
||||
ctx: this.context,
|
||||
line: processedLine
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.command = this.options.command;
|
||||
this.logger.info(`Created new task, id: ${this.id}, process id: ${this.options.process.id}, OS PID: ${this.process.pid}, context: ${this.context}`);
|
||||
}
|
||||
|
||||
kill(): Promise<void> {
|
||||
return new Promise<void>(resolve => {
|
||||
if (this.process.killed) {
|
||||
resolve();
|
||||
} else {
|
||||
const toDispose = this.process.onClose(event => {
|
||||
toDispose.dispose();
|
||||
resolve();
|
||||
});
|
||||
this.process.kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async getTaskExitedEvent(evt: IProcessExitEvent): Promise<TaskExitedEvent> {
|
||||
return {
|
||||
taskId: this.taskId,
|
||||
ctx: this.context,
|
||||
code: evt.code,
|
||||
signal: evt.signal,
|
||||
config: this.options.config,
|
||||
terminalId: this.process.id,
|
||||
processId: this.process.id
|
||||
};
|
||||
}
|
||||
|
||||
getRuntimeInfo(): ProcessTaskInfo {
|
||||
return {
|
||||
taskId: this.id,
|
||||
ctx: this.context,
|
||||
config: this.options.config,
|
||||
terminalId: this.process.id,
|
||||
processId: this.process.id,
|
||||
command: this.command
|
||||
};
|
||||
}
|
||||
|
||||
get process(): Process {
|
||||
return this.options.process;
|
||||
}
|
||||
|
||||
get processType(): ProcessType {
|
||||
return this.options.processType;
|
||||
}
|
||||
}
|
||||
314
packages/task/src/node/task-abstract-line-matcher.ts
Normal file
314
packages/task/src/node/task-abstract-line-matcher.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EOL } from '@theia/core/lib/common/os';
|
||||
import { Diagnostic, DiagnosticSeverity, Range } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import {
|
||||
FileLocationKind, ProblemMatcher, ProblemPattern,
|
||||
ProblemMatch, ProblemMatchData, ProblemLocationKind
|
||||
} from '../common/problem-matcher-protocol';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Severity } from '@theia/core/lib/common/severity';
|
||||
import { MAX_SAFE_INTEGER } from '@theia/core/lib/common/numbers';
|
||||
import { join } from 'path';
|
||||
|
||||
const endOfLine: string = EOL;
|
||||
|
||||
export interface ProblemData {
|
||||
kind?: ProblemLocationKind;
|
||||
file?: string;
|
||||
location?: string;
|
||||
line?: string;
|
||||
character?: string;
|
||||
endLine?: string;
|
||||
endCharacter?: string;
|
||||
message?: string;
|
||||
severity?: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
type ProblemDataStringKeys = keyof { [K in keyof ProblemData as string extends ProblemData[K] ? K : never]: unknown };
|
||||
|
||||
export abstract class AbstractLineMatcher {
|
||||
|
||||
protected patterns: ProblemPattern[] = [];
|
||||
protected activePatternIndex: number = 0;
|
||||
protected activePattern: ProblemPattern | undefined;
|
||||
protected cachedProblemData: ProblemData;
|
||||
|
||||
constructor(
|
||||
protected matcher: ProblemMatcher
|
||||
) {
|
||||
if (Array.isArray(matcher.pattern)) {
|
||||
this.patterns = matcher.pattern;
|
||||
} else {
|
||||
this.patterns = [matcher.pattern];
|
||||
}
|
||||
this.cachedProblemData = this.getEmptyProblemData();
|
||||
|
||||
if (this.patterns.slice(0, this.patternCount - 1).some(p => !!p.loop)) {
|
||||
console.error('Problem Matcher: Only the last pattern can loop');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the problem identified by this line matcher.
|
||||
*
|
||||
* @param line the line of text to find the problem from
|
||||
* @return the identified problem. If the problem is not found, `undefined` is returned.
|
||||
*/
|
||||
abstract match(line: string): ProblemMatch | undefined;
|
||||
|
||||
/**
|
||||
* Number of problem patterns that the line matcher uses.
|
||||
*/
|
||||
get patternCount(): number {
|
||||
return this.patterns.length;
|
||||
}
|
||||
|
||||
protected getEmptyProblemData(): ProblemData {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return Object.create(null) as ProblemData;
|
||||
}
|
||||
|
||||
protected fillProblemData(data: ProblemData | null, pattern: ProblemPattern, matches: RegExpExecArray): data is ProblemData {
|
||||
if (data) {
|
||||
this.fillProperty(data, 'file', pattern, matches, true);
|
||||
this.appendProperty(data, 'message', pattern, matches, true);
|
||||
this.fillProperty(data, 'code', pattern, matches, true);
|
||||
this.fillProperty(data, 'severity', pattern, matches, true);
|
||||
this.fillProperty(data, 'location', pattern, matches, true);
|
||||
this.fillProperty(data, 'line', pattern, matches);
|
||||
this.fillProperty(data, 'character', pattern, matches);
|
||||
this.fillProperty(data, 'endLine', pattern, matches);
|
||||
this.fillProperty(data, 'endCharacter', pattern, matches);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private appendProperty(data: ProblemData, property: ProblemDataStringKeys, pattern: ProblemPattern, matches: RegExpExecArray, trim: boolean = false): void {
|
||||
const patternProperty = pattern[property];
|
||||
if (data[property] === undefined) {
|
||||
this.fillProperty(data, property, pattern, matches, trim);
|
||||
} else if (patternProperty !== undefined && patternProperty < matches.length) {
|
||||
let value = matches[patternProperty];
|
||||
if (trim) {
|
||||
value = value.trim();
|
||||
}
|
||||
data[property] = data[property] + endOfLine + value;
|
||||
}
|
||||
}
|
||||
|
||||
private fillProperty(data: ProblemData, property: ProblemDataStringKeys, pattern: ProblemPattern, matches: RegExpExecArray, trim: boolean = false): void {
|
||||
const patternAtProperty = pattern[property];
|
||||
if (data[property] === undefined && patternAtProperty !== undefined && patternAtProperty < matches.length) {
|
||||
let value = matches[patternAtProperty];
|
||||
if (value !== undefined) {
|
||||
if (trim) {
|
||||
value = value.trim();
|
||||
}
|
||||
data[property] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getMarkerMatch(data: ProblemData): ProblemMatch | undefined {
|
||||
try {
|
||||
const location = this.getLocation(data);
|
||||
if (data.file && location && data.message) {
|
||||
const marker: Diagnostic = {
|
||||
severity: this.getSeverity(data),
|
||||
range: location,
|
||||
message: data.message
|
||||
};
|
||||
if (data.code !== undefined) {
|
||||
marker.code = data.code;
|
||||
}
|
||||
if (this.matcher.source !== undefined) {
|
||||
marker.source = this.matcher.source;
|
||||
}
|
||||
return {
|
||||
description: this.matcher,
|
||||
resource: this.getResource(data.file, this.matcher),
|
||||
marker
|
||||
} as ProblemMatchData;
|
||||
}
|
||||
return {
|
||||
description: this.matcher
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to convert problem data into match: ${JSON.stringify(data)}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getLocation(data: ProblemData): Range | null {
|
||||
if (data.kind === ProblemLocationKind.File) {
|
||||
return this.createRange(0, 0, 0, 0);
|
||||
}
|
||||
if (data.location) {
|
||||
return this.parseLocationInfo(data.location);
|
||||
}
|
||||
if (!data.line) {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return null;
|
||||
}
|
||||
const startLine = parseInt(data.line);
|
||||
const startColumn = data.character ? parseInt(data.character) : undefined;
|
||||
const endLine = data.endLine ? parseInt(data.endLine) : undefined;
|
||||
const endColumn = data.endCharacter ? parseInt(data.endCharacter) : undefined;
|
||||
return this.createRange(startLine, startColumn, endLine, endColumn);
|
||||
}
|
||||
|
||||
private parseLocationInfo(value: string): Range | null {
|
||||
if (!value || !value.match(/(\d+|\d+,\d+|\d+,\d+,\d+,\d+)/)) {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return null;
|
||||
}
|
||||
const parts = value.split(',');
|
||||
const startLine = parseInt(parts[0]);
|
||||
const startColumn = parts.length > 1 ? parseInt(parts[1]) : undefined;
|
||||
if (parts.length > 3) {
|
||||
return this.createRange(startLine, startColumn, parseInt(parts[2]), parseInt(parts[3]));
|
||||
} else {
|
||||
return this.createRange(startLine, startColumn, undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private createRange(startLine: number, startColumn: number | undefined, endLine: number | undefined, endColumn: number | undefined): Range {
|
||||
let range: Range;
|
||||
if (startColumn !== undefined) {
|
||||
if (endColumn !== undefined) {
|
||||
range = Range.create(startLine, startColumn, endLine || startLine, endColumn);
|
||||
} else {
|
||||
range = Range.create(startLine, startColumn, startLine, startColumn);
|
||||
}
|
||||
} else {
|
||||
range = Range.create(startLine, 1, startLine, MAX_SAFE_INTEGER);
|
||||
}
|
||||
|
||||
// range indexes should be zero-based
|
||||
return Range.create(
|
||||
this.getZeroBasedRangeIndex(range.start.line),
|
||||
this.getZeroBasedRangeIndex(range.start.character),
|
||||
this.getZeroBasedRangeIndex(range.end.line),
|
||||
this.getZeroBasedRangeIndex(range.end.character)
|
||||
);
|
||||
}
|
||||
|
||||
private getZeroBasedRangeIndex(ind: number): number {
|
||||
return ind === 0 ? ind : ind - 1;
|
||||
}
|
||||
|
||||
private getSeverity(data: ProblemData): DiagnosticSeverity {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
let result: Severity | null = null;
|
||||
if (data.severity) {
|
||||
const value = data.severity;
|
||||
if (value) {
|
||||
result = Severity.fromValue(value);
|
||||
if (result === Severity.Ignore) {
|
||||
if (value === 'E') {
|
||||
result = Severity.Error;
|
||||
} else if (value === 'W') {
|
||||
result = Severity.Warning;
|
||||
} else if (value === 'I') {
|
||||
result = Severity.Info;
|
||||
} else if (value.toLowerCase() === 'hint') {
|
||||
result = Severity.Info;
|
||||
} else if (value.toLowerCase() === 'note') {
|
||||
result = Severity.Info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
if (result === null || result === Severity.Ignore) {
|
||||
result = this.matcher.severity || Severity.Error;
|
||||
}
|
||||
return Severity.toDiagnosticSeverity(result);
|
||||
}
|
||||
|
||||
private getResource(filename: string, matcher: ProblemMatcher): URI {
|
||||
const kind = matcher.fileLocation;
|
||||
let fullPath: string | undefined;
|
||||
if (kind === FileLocationKind.Absolute) {
|
||||
fullPath = filename;
|
||||
} else if ((kind === FileLocationKind.Relative) && matcher.filePrefix) {
|
||||
let relativeFileName = filename.replace(/\\/g, '/');
|
||||
if (relativeFileName.startsWith('./')) {
|
||||
relativeFileName = relativeFileName.slice(2);
|
||||
}
|
||||
fullPath = join(matcher.filePrefix, relativeFileName);
|
||||
}
|
||||
if (fullPath === undefined) {
|
||||
throw new Error('FileLocationKind is not actionable. Does the matcher have a filePrefix? This should never happen.');
|
||||
}
|
||||
if (matcher.uriProvider !== undefined) {
|
||||
return matcher.uriProvider(fullPath);
|
||||
} else {
|
||||
return URI.fromFilePath(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected resetActivePatternIndex(defaultIndex?: number): void {
|
||||
if (defaultIndex === undefined) {
|
||||
defaultIndex = 0;
|
||||
}
|
||||
this.activePatternIndex = defaultIndex;
|
||||
this.activePattern = this.patterns[defaultIndex];
|
||||
}
|
||||
|
||||
protected nextProblemPattern(): void {
|
||||
this.activePatternIndex++;
|
||||
if (this.activePatternIndex > this.patternCount - 1) {
|
||||
this.resetActivePatternIndex();
|
||||
} else {
|
||||
this.activePattern = this.patterns[this.activePatternIndex];
|
||||
}
|
||||
}
|
||||
|
||||
protected doOneLineMatch(line: string): boolean {
|
||||
if (this.activePattern) {
|
||||
const regexp = new RegExp(this.activePattern.regexp);
|
||||
const regexMatches = regexp.exec(line);
|
||||
if (regexMatches) {
|
||||
this.cachedProblemData.kind ??= this.activePattern.kind;
|
||||
return this.fillProblemData(this.cachedProblemData, this.activePattern, regexMatches);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if active pattern is the last pattern
|
||||
protected isUsingTheLastPattern(): boolean {
|
||||
return this.patternCount > 0 && this.activePatternIndex === this.patternCount - 1;
|
||||
}
|
||||
|
||||
protected isLastPatternLoop(): boolean {
|
||||
return this.patternCount > 0 && !!this.patterns[this.patternCount - 1].loop;
|
||||
}
|
||||
|
||||
protected resetCachedProblemData(): void {
|
||||
this.cachedProblemData = this.getEmptyProblemData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { ContributionProvider } from '@theia/core';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { TaskRunnerContribution, TaskRunnerRegistry } from './task-runner';
|
||||
|
||||
@injectable()
|
||||
export class TaskBackendApplicationContribution implements BackendApplicationContribution {
|
||||
|
||||
@inject(ContributionProvider) @named(TaskRunnerContribution)
|
||||
protected readonly contributionProvider: ContributionProvider<TaskRunnerContribution>;
|
||||
|
||||
@inject(TaskRunnerRegistry)
|
||||
protected readonly taskRunnerRegistry: TaskRunnerRegistry;
|
||||
|
||||
onStart(): void {
|
||||
this.contributionProvider.getContributions().forEach(contrib =>
|
||||
contrib.registerRunner(this.taskRunnerRegistry)
|
||||
);
|
||||
}
|
||||
}
|
||||
59
packages/task/src/node/task-backend-module.ts
Normal file
59
packages/task/src/node/task-backend-module.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { bindContributionProvider } from '@theia/core';
|
||||
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common/messaging';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { bindProcessTaskRunnerModule } from './process/process-task-runner-backend-module';
|
||||
import { bindCustomTaskRunnerModule } from './custom/custom-task-runner-backend-module';
|
||||
import { TaskBackendApplicationContribution } from './task-backend-application-contribution';
|
||||
import { TaskManager } from './task-manager';
|
||||
import { TaskRunnerContribution, TaskRunnerRegistry } from './task-runner';
|
||||
import { TaskServerImpl } from './task-server';
|
||||
import { createCommonBindings } from '../common/task-common-module';
|
||||
import { TaskClient, TaskServer, taskPath } from '../common';
|
||||
import { bindTaskPreferences } from '../common/task-preferences';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
|
||||
bind(TaskManager).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(TaskManager);
|
||||
|
||||
bind(TaskServer).to(TaskServerImpl).inSingletonScope();
|
||||
bind(ConnectionHandler).toDynamicValue(ctx =>
|
||||
new RpcConnectionHandler<TaskClient>(taskPath, client => {
|
||||
const taskServer = ctx.container.get<TaskServer>(TaskServer);
|
||||
taskServer.setClient(client);
|
||||
// when connection closes, cleanup that client of task-server
|
||||
client.onDidCloseConnection(() => {
|
||||
taskServer.disconnectClient(client);
|
||||
});
|
||||
return taskServer;
|
||||
})
|
||||
).inSingletonScope();
|
||||
|
||||
createCommonBindings(bind);
|
||||
|
||||
bind(TaskRunnerRegistry).toSelf().inSingletonScope();
|
||||
bindContributionProvider(bind, TaskRunnerContribution);
|
||||
bind(TaskBackendApplicationContribution).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(TaskBackendApplicationContribution);
|
||||
|
||||
bindProcessTaskRunnerModule(bind);
|
||||
bindCustomTaskRunnerModule(bind);
|
||||
bindTaskPreferences(bind);
|
||||
});
|
||||
127
packages/task/src/node/task-line-matchers.ts
Normal file
127
packages/task/src/node/task-line-matchers.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { AbstractLineMatcher } from './task-abstract-line-matcher';
|
||||
import { ProblemMatcher, ProblemMatch, WatchingPattern } from '../common/problem-matcher-protocol';
|
||||
|
||||
export class StartStopLineMatcher extends AbstractLineMatcher {
|
||||
|
||||
constructor(matcher: ProblemMatcher) {
|
||||
super(matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the problem identified by this line matcher.
|
||||
*
|
||||
* @param line the line of text to find the problem from
|
||||
* @return the identified problem. If the problem is not found, `undefined` is returned.
|
||||
*/
|
||||
match(line: string): ProblemMatch | undefined {
|
||||
if (!this.activePattern) {
|
||||
this.resetActivePatternIndex();
|
||||
}
|
||||
if (this.activePattern) {
|
||||
const originalProblemData = Object.assign(this.getEmptyProblemData(), this.cachedProblemData);
|
||||
const foundMatch = this.doOneLineMatch(line);
|
||||
if (foundMatch) {
|
||||
if (this.isUsingTheLastPattern()) {
|
||||
const matchResult = this.getMarkerMatch(this.cachedProblemData);
|
||||
if (this.isLastPatternLoop()) {
|
||||
this.cachedProblemData = originalProblemData;
|
||||
} else {
|
||||
this.resetCachedProblemData();
|
||||
this.resetActivePatternIndex();
|
||||
}
|
||||
return matchResult;
|
||||
} else {
|
||||
this.nextProblemPattern();
|
||||
}
|
||||
} else {
|
||||
this.resetCachedProblemData();
|
||||
if (this.activePatternIndex !== 0) { // if no match, use the first pattern to parse the same line
|
||||
this.resetActivePatternIndex();
|
||||
return this.match(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class WatchModeLineMatcher extends StartStopLineMatcher {
|
||||
|
||||
private beginsPattern: WatchingPattern;
|
||||
private endsPattern: WatchingPattern;
|
||||
activeOnStart: boolean = false;
|
||||
|
||||
constructor(matcher: ProblemMatcher) {
|
||||
super(matcher);
|
||||
this.beginsPattern = matcher.watching!.beginsPattern;
|
||||
this.endsPattern = matcher.watching!.endsPattern;
|
||||
this.activeOnStart = matcher.watching!.activeOnStart === true;
|
||||
this.resetActivePatternIndex(this.activeOnStart ? 0 : -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the problem identified by this line matcher.
|
||||
*
|
||||
* @param line the line of text to find the problem from
|
||||
* @return the identified problem. If the problem is not found, `undefined` is returned.
|
||||
*/
|
||||
override match(line: string): ProblemMatch | undefined {
|
||||
if (this.activeOnStart) {
|
||||
this.activeOnStart = false;
|
||||
this.resetActivePatternIndex(0);
|
||||
this.resetCachedProblemData();
|
||||
return super.match(line);
|
||||
}
|
||||
|
||||
if (this.matchBegin(line)) {
|
||||
const beginsPatternMatch = this.getMarkerMatch(this.cachedProblemData);
|
||||
this.resetCachedProblemData();
|
||||
return beginsPatternMatch;
|
||||
}
|
||||
if (this.matchEnd(line)) {
|
||||
this.resetCachedProblemData();
|
||||
return undefined;
|
||||
}
|
||||
if (this.activePattern) {
|
||||
return super.match(line);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
matchBegin(line: string): boolean {
|
||||
const beginRegexp = new RegExp(this.beginsPattern.regexp);
|
||||
const regexMatches = beginRegexp.exec(line);
|
||||
if (regexMatches) {
|
||||
this.fillProblemData(this.cachedProblemData, this.beginsPattern, regexMatches);
|
||||
this.resetActivePatternIndex(0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
matchEnd(line: string): boolean {
|
||||
const endRegexp = new RegExp(this.endsPattern.regexp);
|
||||
const match = endRegexp.exec(line);
|
||||
if (match) {
|
||||
this.resetActivePatternIndex(-1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
129
packages/task/src/node/task-manager.ts
Normal file
129
packages/task/src/node/task-manager.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import { Emitter, Event, ILogger } from '@theia/core/lib/common';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { Task } from './task';
|
||||
import { ManagedTaskManager } from '../common';
|
||||
|
||||
// inspired by process-manager.ts
|
||||
|
||||
/**
|
||||
* The {@link TaskManager} is the common component responsible for managing running tasks.
|
||||
*/
|
||||
@injectable()
|
||||
export class TaskManager implements BackendApplicationContribution, ManagedTaskManager<Task> {
|
||||
|
||||
/** contains all running tasks */
|
||||
protected readonly tasks: Map<number, Task> = new Map();
|
||||
/** contains running tasks per context */
|
||||
protected readonly tasksPerCtx: Map<string, Task[]> = new Map();
|
||||
/** each task has this unique task id, for this back-end */
|
||||
protected id: number = -1;
|
||||
/**
|
||||
* Emit when a registered task is deleted.
|
||||
*/
|
||||
protected readonly deleteEmitter = new Emitter<number>();
|
||||
|
||||
constructor(
|
||||
@inject(ILogger) @named('task') protected readonly logger: ILogger
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Registers a new task (in the given context if present). Each registered
|
||||
* task is considered to be currently running.
|
||||
* @param task the new task.
|
||||
* @param ctx the provided context.
|
||||
*
|
||||
* @returns the registration id for the given task.
|
||||
*/
|
||||
register(task: Task, ctx?: string): number {
|
||||
const id = ++this.id;
|
||||
this.tasks.set(id, task);
|
||||
|
||||
if (ctx) {
|
||||
let tks = this.tasksPerCtx.get(ctx);
|
||||
if (tks === undefined) {
|
||||
tks = [];
|
||||
}
|
||||
tks.push(task);
|
||||
this.tasksPerCtx.set(ctx, tks);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to retrieve the registered task for the given id.
|
||||
* @param id the task registration id.
|
||||
*
|
||||
* @returns the task or `undefined` if no task was registered for the given id.
|
||||
*/
|
||||
get(id: number): Task | undefined {
|
||||
return this.tasks.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all running tasks. If a context is provided, filter-down to
|
||||
* only tasks started from that context.
|
||||
* @param ctx the task execution context.
|
||||
*
|
||||
* @returns all running tasks for the given context or `undefined` if no tasks are registered for the given context.
|
||||
*/
|
||||
getTasks(ctx?: string): Task[] | undefined {
|
||||
if (!ctx) {
|
||||
return [...this.tasks.values()];
|
||||
} else {
|
||||
if (this.tasksPerCtx.has(ctx)) {
|
||||
return this.tasksPerCtx.get(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given task from the task manager.
|
||||
* @param task the task to delete.
|
||||
*/
|
||||
delete(task: Task): void {
|
||||
this.tasks.delete(task.id);
|
||||
|
||||
const ctx = task.context;
|
||||
if (ctx && this.tasksPerCtx.has(ctx)) {
|
||||
const tasksForWS = this.tasksPerCtx.get(ctx);
|
||||
if (tasksForWS !== undefined) {
|
||||
const idx = tasksForWS.indexOf(task);
|
||||
if (idx !== -1) {
|
||||
tasksForWS.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.deleteEmitter.fire(task.id);
|
||||
}
|
||||
|
||||
get onDelete(): Event<number> {
|
||||
return this.deleteEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is triggered on application stop to cleanup all ongoing tasks.
|
||||
*/
|
||||
onStop(): void {
|
||||
this.tasks.forEach((task: Task, id: number) => {
|
||||
this.logger.debug(`Task Backend application: onStop(): cleaning task id: ${id}`);
|
||||
this.delete(task);
|
||||
});
|
||||
}
|
||||
}
|
||||
338
packages/task/src/node/task-problem-collector.spec.ts
Normal file
338
packages/task/src/node/task-problem-collector.spec.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Severity } from '@theia/core/lib/common/severity';
|
||||
import { DiagnosticSeverity } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { expect } from 'chai';
|
||||
import { ApplyToKind, FileLocationKind, ProblemLocationKind, ProblemMatch, ProblemMatchData, ProblemMatcher } from '../common/problem-matcher-protocol';
|
||||
import { ProblemCollector } from './task-problem-collector';
|
||||
|
||||
const startStopMatcher1: ProblemMatcher = {
|
||||
owner: 'test1',
|
||||
source: 'test1',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: {
|
||||
regexp: /^([^:]*: )?((.:)?[^:]*):(\d+)(:(\d+))?: (.*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 2,
|
||||
line: 4,
|
||||
character: 6,
|
||||
message: 7
|
||||
},
|
||||
severity: Severity.Error
|
||||
};
|
||||
|
||||
const startStopMatcher2: ProblemMatcher = {
|
||||
owner: 'test2',
|
||||
source: 'test2',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: [
|
||||
{
|
||||
regexp: /^([^\s].*)$/.source,
|
||||
kind: ProblemLocationKind.Location,
|
||||
file: 1
|
||||
},
|
||||
{
|
||||
regexp: /^\s+(\d+):(\d+)\s+(error|warning|info)\s+(.+?)(?:\s\s+(.*))?$/.source,
|
||||
line: 1,
|
||||
character: 2,
|
||||
severity: 3,
|
||||
message: 4,
|
||||
code: 5,
|
||||
loop: true
|
||||
}
|
||||
],
|
||||
severity: Severity.Error
|
||||
};
|
||||
|
||||
const startStopMatcher3: ProblemMatcher = {
|
||||
owner: 'test2',
|
||||
source: 'test2',
|
||||
applyTo: ApplyToKind.allDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: [
|
||||
{
|
||||
regexp: /^([^\s].*)$/.source,
|
||||
kind: ProblemLocationKind.File,
|
||||
file: 1
|
||||
},
|
||||
{
|
||||
regexp: /^\s+(\d+):(\d+)\s+(error|warning|info)\s+(.+?)(?:\s\s+(.*))?$/.source,
|
||||
severity: 3,
|
||||
message: 4,
|
||||
code: 5,
|
||||
loop: true
|
||||
}
|
||||
],
|
||||
severity: Severity.Error
|
||||
};
|
||||
|
||||
const watchMatcher: ProblemMatcher = {
|
||||
owner: 'test3',
|
||||
applyTo: ApplyToKind.closedDocuments,
|
||||
fileLocation: FileLocationKind.Absolute,
|
||||
pattern: {
|
||||
regexp: /Error: ([^(]+)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\): (.*)$/.source,
|
||||
file: 1,
|
||||
location: 2,
|
||||
message: 3
|
||||
},
|
||||
watching: {
|
||||
activeOnStart: false,
|
||||
beginsPattern: { regexp: /Starting compilation/.source },
|
||||
endsPattern: { regexp: /Finished compilation/.source }
|
||||
}
|
||||
};
|
||||
|
||||
describe('ProblemCollector', () => {
|
||||
let collector: ProblemCollector;
|
||||
const allMatches: ProblemMatch[] = [];
|
||||
|
||||
const collectMatches = (lines: string[]) => {
|
||||
lines.forEach(line => {
|
||||
const matches = collector.processLine(line);
|
||||
if (matches.length > 0) {
|
||||
allMatches.push(...matches);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
allMatches.length = 0;
|
||||
});
|
||||
|
||||
it('should find problems from start-stop task when problem matcher is associated with one problem pattern', () => {
|
||||
collector = new ProblemCollector([startStopMatcher1]);
|
||||
collectMatches([
|
||||
'npm WARN lifecycle The node binary used for scripts is /tmp/yarn--1557403301319-0.5645247996849125/node but npm is using /usr/local/bin/node itself.',
|
||||
'Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.',
|
||||
'',
|
||||
'# command-line-arguments',
|
||||
'/home/test/hello.go:9:2: undefined: fmt.Pntln',
|
||||
'/home/test/hello.go:10:6: undefined: numb',
|
||||
'/home/test/hello.go:15:9: undefined: stri'
|
||||
]);
|
||||
|
||||
expect(allMatches.length).to.eq(3);
|
||||
|
||||
expect((allMatches[0] as ProblemMatchData).resource!.path.toString()).eq('/home/test/hello.go');
|
||||
expect((allMatches[0] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 8, character: 1 }, end: { line: 8, character: 1 } },
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'test1',
|
||||
message: 'undefined: fmt.Pntln'
|
||||
});
|
||||
|
||||
expect((allMatches[1] as ProblemMatchData).resource!.path.toString()).eq('/home/test/hello.go');
|
||||
expect((allMatches[1] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 9, character: 5 }, end: { line: 9, character: 5 } },
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'test1',
|
||||
message: 'undefined: numb'
|
||||
});
|
||||
|
||||
expect((allMatches[2] as ProblemMatchData).resource!.path.toString()).eq('/home/test/hello.go');
|
||||
expect((allMatches[2] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 14, character: 8 }, end: { line: 14, character: 8 } },
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'test1',
|
||||
message: 'undefined: stri'
|
||||
});
|
||||
});
|
||||
|
||||
it('should find problems from start-stop task when problem matcher is associated with more than one problem pattern', () => {
|
||||
collector = new ProblemCollector([startStopMatcher2]);
|
||||
collectMatches([
|
||||
'> test@0.1.0 lint /home/test',
|
||||
'> eslint .',
|
||||
'',
|
||||
'',
|
||||
'/home/test/test-dir.js',
|
||||
' 14:21 warning Missing semicolon semi',
|
||||
' 15:23 warning Missing semicolon semi',
|
||||
' 103:9 error Parsing error: Unexpected token inte',
|
||||
'',
|
||||
'/home/test/more-test.js',
|
||||
' 13:9 error Parsing error: Unexpected token 1000',
|
||||
'',
|
||||
'✖ 3 problems (1 error, 2 warnings)',
|
||||
' 0 errors and 2 warnings potentially fixable with the `--fix` option.'
|
||||
]);
|
||||
|
||||
expect(allMatches.length).to.eq(4);
|
||||
|
||||
expect((allMatches[0] as ProblemMatchData).resource!.path.toString()).eq('/home/test/test-dir.js');
|
||||
expect((allMatches[0] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 13, character: 20 }, end: { line: 13, character: 20 } },
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
source: 'test2',
|
||||
message: 'Missing semicolon',
|
||||
code: 'semi'
|
||||
});
|
||||
|
||||
expect((allMatches[1] as ProblemMatchData).resource!.path.toString()).eq('/home/test/test-dir.js');
|
||||
expect((allMatches[1] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 14, character: 22 }, end: { line: 14, character: 22 } },
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
source: 'test2',
|
||||
message: 'Missing semicolon',
|
||||
code: 'semi'
|
||||
});
|
||||
|
||||
expect((allMatches[2] as ProblemMatchData).resource!.path.toString()).eq('/home/test/test-dir.js');
|
||||
expect((allMatches[2] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 102, character: 8 }, end: { line: 102, character: 8 } },
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'test2',
|
||||
message: 'Parsing error: Unexpected token inte'
|
||||
});
|
||||
|
||||
expect((allMatches[3] as ProblemMatchData).resource!.path.toString()).eq('/home/test/more-test.js');
|
||||
expect((allMatches[3] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 12, character: 8 }, end: { line: 12, character: 8 } },
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'test2',
|
||||
message: 'Parsing error: Unexpected token 1000'
|
||||
});
|
||||
});
|
||||
|
||||
it('should find problems from start-stop task when problem matcher is associated with more than one problem pattern and kind is file', () => {
|
||||
collector = new ProblemCollector([startStopMatcher3]);
|
||||
collectMatches([
|
||||
'> test@0.1.0 lint /home/test',
|
||||
'> eslint .',
|
||||
'',
|
||||
'',
|
||||
'/home/test/test-dir.js',
|
||||
' 14:21 warning Missing semicolon semi',
|
||||
' 15:23 warning Missing semicolon semi',
|
||||
' 103:9 error Parsing error: Unexpected token inte',
|
||||
'',
|
||||
'/home/test/more-test.js',
|
||||
' 13:9 error Parsing error: Unexpected token 1000',
|
||||
'',
|
||||
'✖ 3 problems (1 error, 2 warnings)',
|
||||
' 0 errors and 2 warnings potentially fixable with the `--fix` option.'
|
||||
]);
|
||||
|
||||
expect(allMatches.length).to.eq(4);
|
||||
|
||||
expect((allMatches[0] as ProblemMatchData).resource?.path.toString()).eq('/home/test/test-dir.js');
|
||||
expect((allMatches[0] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
source: 'test2',
|
||||
message: 'Missing semicolon',
|
||||
code: 'semi'
|
||||
});
|
||||
|
||||
expect((allMatches[1] as ProblemMatchData).resource?.path.toString()).eq('/home/test/test-dir.js');
|
||||
expect((allMatches[1] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
source: 'test2',
|
||||
message: 'Missing semicolon',
|
||||
code: 'semi'
|
||||
});
|
||||
|
||||
expect((allMatches[2] as ProblemMatchData).resource?.path.toString()).eq('/home/test/test-dir.js');
|
||||
expect((allMatches[2] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'test2',
|
||||
message: 'Parsing error: Unexpected token inte'
|
||||
});
|
||||
|
||||
expect((allMatches[3] as ProblemMatchData).resource?.path.toString()).eq('/home/test/more-test.js');
|
||||
expect((allMatches[3] as ProblemMatchData).marker).deep.equal({
|
||||
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'test2',
|
||||
message: 'Parsing error: Unexpected token 1000'
|
||||
});
|
||||
});
|
||||
|
||||
it('should search and find defined problems from watch task\'s output', () => {
|
||||
collector = new ProblemCollector([watchMatcher]);
|
||||
|
||||
collectMatches([
|
||||
'> code-oss-dev@1.33.1 watch /home/test/vscode',
|
||||
'> gulp watch --max_old_space_size=4095',
|
||||
'',
|
||||
'[09:15:37] Node flags detected: --max_old_space_size=4095',
|
||||
'[09:15:37] Respawned to PID: 14560',
|
||||
'[09:15:40] Using gulpfile ~/dev/vscode/gulpfile.js',
|
||||
"[09:15:40] Starting 'watch'...",
|
||||
'[09:15:40] Starting clean-out ...',
|
||||
'[09:15:41] Starting clean-extension-configuration-editing ...',
|
||||
'[09:15:41] Starting clean-extension-css-language-features-client ...',
|
||||
'[09:15:41] Starting clean-extension-css-language-features-server ...',
|
||||
'[09:15:41] Starting clean-extension-debug-auto-launch ...',
|
||||
'[09:15:41] Starting watch-extension:markdown-language-features-preview-src ...',
|
||||
'[09:15:41] Starting compilation...', // begin pattern 1
|
||||
'[09:15:41] Finished clean-extension-typescript-basics-test-colorize-fixtures after 49 ms',
|
||||
'[09:15:41] Starting watch-extension:typescript-basics-test-colorize-fixtures ...',
|
||||
'[09:15:41] Finished compilation with 0 errors after 30 ms',
|
||||
'[09:15:41] Finished clean-extension-css-language-features-client after 119 ms',
|
||||
'[09:15:41] Starting watch-extension:css-language-features-client ...',
|
||||
'[09:15:41] Starting compilation...', // begin pattern 2
|
||||
'[09:15:41] Finished clean-extension-configuration-editing after 128 ms',
|
||||
'[09:15:41] Starting watch-extension:configuration-editing ...',
|
||||
'[09:15:41] Finished clean-extension-debug-auto-launch after 133 ms',
|
||||
'[09:15:41] Starting watch-extension:debug-auto-launch ...',
|
||||
'[09:15:41] Finished clean-extension-debug-server-ready after 138 ms',
|
||||
'[09:15:52] Starting watch-extension:html-language-features-server ...',
|
||||
'[09:15:58] Finished clean-out after 17196 ms',
|
||||
'[09:15:58] Starting watch-client ...',
|
||||
'[09:17:25] Finished compilation with 0 errors after 104209 ms',
|
||||
'[09:19:22] Starting compilation...', // begin pattern 3
|
||||
"[09:19:23] Error: /home/test/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts(517,19): ')' expected.", // problem 1
|
||||
"[09:19:23] Error: /home/test/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts(517,57): ';' expected.", // problem 2
|
||||
'[09:19:23] Finished compilation with 2 errors after 1051 ms',
|
||||
'[09:20:21] Starting compilation...', // begin pattern 4
|
||||
"[09:20:24] Error: /home/test/src/vs/workbench/contrib/tasks/common/problemCollectors.ts(15,30): Cannot find module 'n/uuid'.", // problem 3
|
||||
"[09:20:24] Error: /home/test/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts(517,19): ')' expected.", // problem 4
|
||||
"[09:20:24] Error: /home/test/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts(517,57): ';' expected.", // problem 5
|
||||
'[09:20:24] Finished compilation with 3 errors after 2586 ms',
|
||||
'[09:20:24] Starting compilation...', // begin pattern 5
|
||||
'[09:20:25] Error: /home/test/src/vs/workbench/contrib/tasks/common/taskTemplates.ts(12,14): Type expected.', // problem 6
|
||||
"[09:20:25] Error: /home/test/src/vs/workbench/contrib/tasks/common/problemCollectors.ts(15,30): Cannot find module 'n/uuid'.", // problem 7
|
||||
"[09:20:25] Error: /home/test/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts(517,19): ')' expected.", // problem 8
|
||||
"[09:20:25] Error: /home/test/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts(517,57): ';' expected.", // problem 9
|
||||
'[09:20:25] Finished compilation with 4 errors after 441 ms'
|
||||
]);
|
||||
|
||||
expect(allMatches.length).to.eq(14); // 9 events for problems + 5 events for beginner pattern
|
||||
});
|
||||
|
||||
it('should return an empty array if no problems are found', () => {
|
||||
collector = new ProblemCollector([startStopMatcher2]);
|
||||
|
||||
collectMatches([]);
|
||||
expect(allMatches.length).to.eq(0);
|
||||
|
||||
collectMatches([
|
||||
'> test@0.1.0 lint /home/test',
|
||||
'> eslint .',
|
||||
'',
|
||||
'',
|
||||
'0 problems (0 error, 0 warnings)',
|
||||
]);
|
||||
expect(allMatches.length).to.eq(0);
|
||||
});
|
||||
});
|
||||
62
packages/task/src/node/task-problem-collector.ts
Normal file
62
packages/task/src/node/task-problem-collector.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { AbstractLineMatcher } from './task-abstract-line-matcher';
|
||||
import { ProblemMatcher, ProblemMatch } from '../common/problem-matcher-protocol';
|
||||
import { StartStopLineMatcher, WatchModeLineMatcher } from './task-line-matchers';
|
||||
|
||||
export class ProblemCollector {
|
||||
|
||||
private lineMatchers: AbstractLineMatcher[] = [];
|
||||
|
||||
constructor(
|
||||
public problemMatchers: ProblemMatcher[]
|
||||
) {
|
||||
for (const matcher of problemMatchers) {
|
||||
if (ProblemMatcher.isWatchModeWatcher(matcher)) {
|
||||
this.lineMatchers.push(new WatchModeLineMatcher(matcher));
|
||||
} else {
|
||||
this.lineMatchers.push(new StartStopLineMatcher(matcher));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processLine(line: string): ProblemMatch[] {
|
||||
const markers: ProblemMatch[] = [];
|
||||
this.lineMatchers.forEach(lineMatcher => {
|
||||
const match = lineMatcher.match(line);
|
||||
if (match) {
|
||||
markers.push(match);
|
||||
}
|
||||
});
|
||||
return markers;
|
||||
}
|
||||
|
||||
isTaskActiveOnStart(): boolean {
|
||||
const activeOnStart = this.lineMatchers.some(lineMatcher => (lineMatcher instanceof WatchModeLineMatcher) && lineMatcher.activeOnStart);
|
||||
return activeOnStart;
|
||||
}
|
||||
|
||||
matchBeginMatcher(line: string): boolean {
|
||||
const match = this.lineMatchers.some(lineMatcher => (lineMatcher instanceof WatchModeLineMatcher) && lineMatcher.matchBegin(line));
|
||||
return match;
|
||||
}
|
||||
|
||||
matchEndMatcher(line: string): boolean {
|
||||
const match = this.lineMatchers.some(lineMatcher => (lineMatcher instanceof WatchModeLineMatcher) && lineMatcher.matchEnd(line));
|
||||
return match;
|
||||
}
|
||||
}
|
||||
33
packages/task/src/node/task-runner-protocol.ts
Normal file
33
packages/task/src/node/task-runner-protocol.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { TaskConfiguration } from '../common';
|
||||
import { Task } from './task';
|
||||
|
||||
export const TaskRunner = Symbol('TaskRunner');
|
||||
/**
|
||||
* A {@link TaskRunner} knows how to run a task configuration of a particular type.
|
||||
*/
|
||||
export interface TaskRunner {
|
||||
/**
|
||||
* Runs a task based on the given `TaskConfiguration`.
|
||||
* @param taskConfig the task configuration that should be executed.
|
||||
* @param ctx the execution context.
|
||||
*
|
||||
* @returns a promise of the (currently running) {@link Task}.
|
||||
*/
|
||||
run(tskConfig: TaskConfiguration, ctx?: string): Promise<Task>;
|
||||
}
|
||||
96
packages/task/src/node/task-runner.ts
Normal file
96
packages/task/src/node/task-runner.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// *****************************************************************************
|
||||
// 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { ProcessTaskRunner } from './process/process-task-runner';
|
||||
import { TaskRunner } from './task-runner-protocol';
|
||||
export { TaskRunner };
|
||||
|
||||
export const TaskRunnerContribution = Symbol('TaskRunnerContribution');
|
||||
|
||||
/** The {@link TaskRunnerContribution} can be used to contribute custom {@link TaskRunner}s. */
|
||||
export interface TaskRunnerContribution {
|
||||
/**
|
||||
* Register custom runners using the given {@link TaskRunnerRegistry}.
|
||||
* @param runners the common task runner registry.
|
||||
*/
|
||||
registerRunner(runners: TaskRunnerRegistry): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link TaskRunnerRegistry} is the common component for the registration and provisioning of
|
||||
* {@link TaskRunner}s. Theia will collect all {@link TaskRunner}s and invoke {@link TaskRunnerContribution#registerRunner}
|
||||
* for each contribution. The `TaskServer` will use the runners provided by this registry to execute `TaskConfiguration`s that
|
||||
* have been triggered by the user.
|
||||
*/
|
||||
@injectable()
|
||||
export class TaskRunnerRegistry {
|
||||
|
||||
protected runners: Map<string, TaskRunner>;
|
||||
/** A Task Runner that will be used for executing a Task without an associated Runner. */
|
||||
protected defaultRunner: TaskRunner;
|
||||
|
||||
@inject(ProcessTaskRunner)
|
||||
protected readonly processTaskRunner: ProcessTaskRunner;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.runners = new Map();
|
||||
this.defaultRunner = this.processTaskRunner;
|
||||
}
|
||||
/**
|
||||
* Registers the given {@link TaskRunner} to execute Tasks of the specified type.
|
||||
* If there is already a {@link TaskRunner} registered for the specified type the registration will
|
||||
* be overwritten with the new value.
|
||||
* @param type the task type for which the given runner should be registered.
|
||||
* @param runner the task runner that should be registered.
|
||||
*
|
||||
* @returns a `Disposable` that can be invoked to unregister the given runner.
|
||||
*/
|
||||
registerRunner(type: string, runner: TaskRunner): Disposable {
|
||||
this.runners.set(type, runner);
|
||||
return {
|
||||
dispose: () => this.runners.delete(type)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for a registered {@link TaskRunner} for each of the task types in sequence and returns the first that is found
|
||||
* If no task runner is registered for any of the types, the default runner is returned.
|
||||
* @param types the task types.
|
||||
*
|
||||
* @returns the registered {@link TaskRunner} or a default runner if none is registered for the specified types.
|
||||
*/
|
||||
getRunner(...types: string[]): TaskRunner {
|
||||
for (const type of types) {
|
||||
const runner = this.runners.get(type);
|
||||
if (runner) {
|
||||
return runner;
|
||||
}
|
||||
}
|
||||
return this.defaultRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives all task types for which a {@link TaskRunner} is registered.
|
||||
*
|
||||
* @returns all derived task types.
|
||||
*/
|
||||
getRunnerTypes(): string[] {
|
||||
return [...this.runners.keys()];
|
||||
}
|
||||
}
|
||||
443
packages/task/src/node/task-server.slow-spec.ts
Normal file
443
packages/task/src/node/task-server.slow-spec.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017-2019 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import 'reflect-metadata';
|
||||
import { createTaskTestContainer } from './test/task-test-container';
|
||||
import { BackendApplication } from '@theia/core/lib/node/backend-application';
|
||||
import { TaskExitedEvent, TaskInfo, TaskServer, TaskWatcher, TaskConfiguration } from '../common';
|
||||
import { ProcessType, ProcessTaskConfiguration } from '../common/process/task-protocol';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { isWindows, isOSX } from '@theia/core/lib/common/os';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import { terminalsPath } from '@theia/terminal/lib/common/terminal-protocol';
|
||||
import { TestWebSocketChannelSetup } from '@theia/core/lib/node/messaging/test/test-web-socket-channel';
|
||||
import { expect } from 'chai';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { StringBufferingStream } from '@theia/terminal/lib/node/buffering-stream';
|
||||
import { BackendApplicationConfigProvider } from '@theia/core/lib/node/backend-application-config-provider';
|
||||
|
||||
// test scripts that we bundle with tasks
|
||||
const commandShortRunning = './task';
|
||||
const commandShortRunningOsx = './task-osx';
|
||||
const commandShortRunningWindows = '.\\task.bat';
|
||||
|
||||
const commandLongRunning = './task-long-running';
|
||||
const commandLongRunningOsx = './task-long-running-osx';
|
||||
const commandLongRunningWindows = '.\\task-long-running.bat';
|
||||
|
||||
const bogusCommand = 'thisisnotavalidcommand';
|
||||
|
||||
const commandUnixNoop = 'true';
|
||||
const commandWindowsNoop = 'rundll32.exe';
|
||||
|
||||
/** Expects argv to be ['a', 'b', 'c'] */
|
||||
const script0 = './test-arguments-0.js';
|
||||
/** Expects argv to be ['a', 'b', ' c'] */
|
||||
const script1 = './test-arguments-1.js';
|
||||
/** Expects argv to be ['a', 'b', 'c"'] */
|
||||
const script2 = './test-arguments-2.js';
|
||||
|
||||
// we use test-resources subfolder ('<theia>/packages/task/test-resources/'),
|
||||
// as workspace root, for these tests
|
||||
const wsRootUri: URI = FileUri.create(__dirname).resolve('../../test-resources');
|
||||
const wsRoot: string = FileUri.fsPath(wsRootUri);
|
||||
|
||||
describe('Task server / back-end', function (): void {
|
||||
this.timeout(20000);
|
||||
|
||||
let backend: BackendApplication;
|
||||
let server: http.Server | https.Server;
|
||||
let taskServer: TaskServer;
|
||||
let taskWatcher: TaskWatcher;
|
||||
|
||||
this.beforeAll(() => {
|
||||
BackendApplicationConfigProvider.set({});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
delete process.env['THEIA_TASK_TEST_DEBUG'];
|
||||
const testContainer = createTaskTestContainer();
|
||||
taskWatcher = testContainer.get(TaskWatcher);
|
||||
taskServer = testContainer.get(TaskServer);
|
||||
taskServer.setClient(taskWatcher.getTaskClient());
|
||||
backend = testContainer.get(BackendApplication);
|
||||
server = await backend.start(3000, 'localhost');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const _backend = backend;
|
||||
const _server = server;
|
||||
backend = undefined!;
|
||||
taskServer = undefined!;
|
||||
taskWatcher = undefined!;
|
||||
server = undefined!;
|
||||
_backend['onStop']();
|
||||
_server.close();
|
||||
});
|
||||
|
||||
it('task running in terminal - expected data is received from the terminal ws server', async function (): Promise<void> {
|
||||
const someString = 'someSingleWordString';
|
||||
|
||||
// create task using terminal process
|
||||
const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
|
||||
const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot);
|
||||
const terminalId = taskInfo.terminalId;
|
||||
|
||||
const messagesToWaitFor = 10;
|
||||
const messages: string[] = [];
|
||||
|
||||
// check output of task on terminal is what we expect
|
||||
const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`;
|
||||
|
||||
// hook-up to terminal's ws and confirm that it outputs expected tasks' output
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const setup = new TestWebSocketChannelSetup({ server, path: `${terminalsPath}/${terminalId}` });
|
||||
const stringBuffer = new StringBufferingStream();
|
||||
setup.connectionProvider.listen(`${terminalsPath}/${terminalId}`, (path, channel) => {
|
||||
channel.onMessage(e => stringBuffer.push(e().readString()));
|
||||
channel.onError(reject);
|
||||
channel.onClose(() => reject(new Error('Channel has been closed')));
|
||||
}, false);
|
||||
stringBuffer.onData(currentMessage => {
|
||||
// Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected.
|
||||
// For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n`
|
||||
messages.unshift(currentMessage);
|
||||
if (currentMessage.includes(expected)) {
|
||||
resolve();
|
||||
} else if (messages.length >= messagesToWaitFor) {
|
||||
reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('task using raw process - task server success response shall not contain a terminal id', async function (): Promise<void> {
|
||||
const someString = 'someSingleWordString';
|
||||
const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
|
||||
const executable = FileUri.fsPath(wsRootUri.resolve(command));
|
||||
|
||||
// create task using raw process
|
||||
const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', executable, [someString]), wsRoot);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const toDispose = taskWatcher.onTaskExit((event: TaskExitedEvent) => {
|
||||
if (event.taskId === taskInfo.taskId && event.code === 0) {
|
||||
if (typeof taskInfo.terminalId === 'number') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`terminal id was expected to be a number, got: ${typeof taskInfo.terminalId}`));
|
||||
}
|
||||
toDispose.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('task is executed successfully with cwd as a file URI', async function (): Promise<void> {
|
||||
const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
|
||||
const config = createProcessTaskConfig('shell', command, undefined, FileUri.create(wsRoot).toString());
|
||||
const taskInfo: TaskInfo = await taskServer.run(config, wsRoot);
|
||||
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
|
||||
});
|
||||
|
||||
it('task is executed successfully using terminal process', async function (): Promise<void> {
|
||||
const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
|
||||
const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', command, undefined), wsRoot);
|
||||
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
|
||||
});
|
||||
|
||||
it('task is executed successfully using raw process', async function (): Promise<void> {
|
||||
const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
|
||||
const executable = FileUri.fsPath(wsRootUri.resolve(command));
|
||||
const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', executable, []));
|
||||
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
|
||||
});
|
||||
|
||||
it('task without a specific runner is executed successfully using as a process', async function (): Promise<void> {
|
||||
const command = isWindows ? commandWindowsNoop : commandUnixNoop;
|
||||
|
||||
// there's no runner registered for the 'npm' task type
|
||||
const taskConfig: TaskConfiguration = createTaskConfig('npm', command, []);
|
||||
const taskInfo: TaskInfo = await taskServer.run(taskConfig, wsRoot);
|
||||
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
|
||||
});
|
||||
|
||||
it('task can successfully execute command found in system path using a terminal process', async function (): Promise<void> {
|
||||
const command = isWindows ? commandWindowsNoop : commandUnixNoop;
|
||||
const opts: TaskConfiguration = createProcessTaskConfig('shell', command, []);
|
||||
const taskInfo: TaskInfo = await taskServer.run(opts, wsRoot);
|
||||
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
|
||||
});
|
||||
|
||||
it('task can successfully execute command found in system path using a raw process', async function (): Promise<void> {
|
||||
const command = isWindows ? commandWindowsNoop : commandUnixNoop;
|
||||
const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', command, []), wsRoot);
|
||||
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
|
||||
});
|
||||
|
||||
it('task using type "shell" can be killed', async function (): Promise<void> {
|
||||
const taskInfo: TaskInfo = await taskServer.run(createTaskConfigTaskLongRunning('shell'), wsRoot);
|
||||
|
||||
const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
|
||||
taskServer.kill(taskInfo.taskId);
|
||||
const exitStatus = await exitStatusPromise;
|
||||
|
||||
// node-pty reports different things on Linux/macOS vs Windows when
|
||||
// killing a process. This is not ideal, but that's how things are
|
||||
// currently. Ideally, its behavior should be aligned as much as
|
||||
// possible on what node's child_process module does.
|
||||
if (isWindows) {
|
||||
// On Windows, node-pty just reports an exit code of 0.
|
||||
// expect(exitStatus).equals(1); // this does not work reliably: locally, the exit code from node-pty is 0, whereas in CI it is 1
|
||||
} else {
|
||||
// On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
|
||||
expect(exitStatus).equals('SIGHUP');
|
||||
}
|
||||
});
|
||||
|
||||
it('task using type "process" can be killed', async function (): Promise<void> {
|
||||
const taskInfo: TaskInfo = await taskServer.run(createTaskConfigTaskLongRunning('process'), wsRoot);
|
||||
|
||||
const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
|
||||
taskServer.kill(taskInfo.taskId);
|
||||
const exitStatus = await exitStatusPromise;
|
||||
|
||||
// node-pty reports different things on Linux/macOS vs Windows when
|
||||
// killing a process. This is not ideal, but that's how things are
|
||||
// currently. Ideally, its behavior should be aligned as much as
|
||||
// possible on what node's child_process module does.
|
||||
if (isWindows) {
|
||||
// On Windows, node-pty just reports an exit code of 1.
|
||||
// expect(exitStatus).equals(1); // this does not work reliably: locally, the exit code from node-pty is 0, whereas in CI it is 1
|
||||
} else {
|
||||
// On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
|
||||
expect(exitStatus).equals('SIGHUP');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO: Figure out how to debug a process that correctly starts but exits with a return code > 0
|
||||
*/
|
||||
it('task using terminal process can handle command that does not exist', async function (): Promise<void> {
|
||||
const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig2('shell', bogusCommand, []), wsRoot);
|
||||
const code = await new Promise<number>((resolve, reject) => {
|
||||
taskWatcher.onTaskExit((event: TaskExitedEvent) => {
|
||||
if (event.taskId !== taskInfo.taskId || event.code === undefined) {
|
||||
reject(new Error(JSON.stringify(event)));
|
||||
} else {
|
||||
resolve(event.code);
|
||||
}
|
||||
});
|
||||
});
|
||||
// node-pty reports different things on Linux/macOS vs Windows when
|
||||
// killing a process. This is not ideal, but that's how things are
|
||||
// currently. Ideally, its behavior should be aligned as much as
|
||||
// possible on what node's child_process module does.
|
||||
if (isWindows) {
|
||||
expect(code).equals(1);
|
||||
} else {
|
||||
expect(code).equals(127);
|
||||
}
|
||||
});
|
||||
|
||||
it('getTasks(ctx) returns tasks according to created context', async function (): Promise<void> {
|
||||
const context1 = 'aContext';
|
||||
const context2 = 'anotherContext';
|
||||
|
||||
// create some tasks: 4 for context1, 2 for context2
|
||||
const task1 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
|
||||
const task2 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
|
||||
const task3 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
|
||||
const task4 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
|
||||
const task5 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
|
||||
const task6 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context1);
|
||||
|
||||
const runningTasksCtx1 = await taskServer.getTasks(context1); // should return 4 tasks
|
||||
const runningTasksCtx2 = await taskServer.getTasks(context2); // should return 2 tasks
|
||||
const runningTasksAll = await taskServer.getTasks(); // should return 6 tasks
|
||||
|
||||
if (runningTasksCtx1.length !== 4) {
|
||||
throw new Error(`Error: unexpected number of running tasks for context 1: expected: 4, actual: ${runningTasksCtx1.length}`);
|
||||
} if (runningTasksCtx2.length !== 2) {
|
||||
throw new Error(`Error: unexpected number of running tasks for context 2: expected: 2, actual: ${runningTasksCtx1.length}`);
|
||||
} if (runningTasksAll.length !== 6) {
|
||||
throw new Error(`Error: unexpected total number of running tasks for all contexts: expected: 6, actual: ${runningTasksCtx1.length}`);
|
||||
}
|
||||
|
||||
// cleanup
|
||||
await taskServer.kill(task1.taskId);
|
||||
await taskServer.kill(task2.taskId);
|
||||
await taskServer.kill(task3.taskId);
|
||||
await taskServer.kill(task4.taskId);
|
||||
await taskServer.kill(task5.taskId);
|
||||
await taskServer.kill(task6.taskId);
|
||||
});
|
||||
|
||||
it('creating and killing a bunch of tasks works as expected', async function (): Promise<void> {
|
||||
// const command = isWindows ? command_absolute_path_long_running_windows : command_absolute_path_long_running;
|
||||
const numTasks = 20;
|
||||
const taskInfo: TaskInfo[] = [];
|
||||
|
||||
// create a mix of terminal and raw processes
|
||||
for (let i = 0; i < numTasks; i++) {
|
||||
if (i % 2 === 0) {
|
||||
taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('shell')));
|
||||
} else {
|
||||
taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('process')));
|
||||
}
|
||||
}
|
||||
|
||||
const numRunningTasksAfterCreated = await taskServer.getTasks();
|
||||
|
||||
for (let i = 0; i < taskInfo.length; i++) {
|
||||
await taskServer.kill(taskInfo[i].taskId);
|
||||
}
|
||||
const numRunningTasksAfterKilled = await taskServer.getTasks();
|
||||
|
||||
if (numRunningTasksAfterCreated.length !== numTasks) {
|
||||
throw new Error(`Error: unexpected number of running tasks: expected: ${numTasks}, actual: ${numRunningTasksAfterCreated.length}`);
|
||||
} if (numRunningTasksAfterKilled.length !== 0) {
|
||||
throw new Error(`Error: remaining running tasks, after all killed: expected: 0, actual: ${numRunningTasksAfterKilled.length}`);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
it('shell task should execute the command as a whole if not arguments are specified', async function (): Promise<void> {
|
||||
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0a a b c`));
|
||||
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
|
||||
expect(exitStatus).eq(0);
|
||||
});
|
||||
|
||||
it('shell task should fail if user defines a full command line and arguments', async function (): Promise<void> {
|
||||
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0b a b c`, []));
|
||||
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
|
||||
expect(exitStatus).not.eq(0);
|
||||
});
|
||||
|
||||
it('shell task should be able to exec using simple arguments', async function (): Promise<void> {
|
||||
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script0, 'debug-hint:0c', 'a', 'b', 'c']));
|
||||
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
|
||||
expect(exitStatus).eq(0);
|
||||
});
|
||||
|
||||
it('shell task should be able to run using arguments containing whitespace', async function (): Promise<void> {
|
||||
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script1, 'debug-hint:1', 'a', 'b', ' c']));
|
||||
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
|
||||
expect(exitStatus).eq(0);
|
||||
});
|
||||
|
||||
it('shell task will fail if user specify problematic arguments', async function (): Promise<void> {
|
||||
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2a', 'a', 'b', 'c"']));
|
||||
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
|
||||
expect(exitStatus).not.eq(0);
|
||||
});
|
||||
|
||||
it('shell task should be able to run using arguments specifying which quoting method to use', async function (): Promise<void> {
|
||||
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2b', 'a', 'b', { value: 'c"', quoting: 'escape' }]));
|
||||
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
|
||||
expect(exitStatus).eq(0);
|
||||
});
|
||||
|
||||
it('shell task should be able to run using arguments with forbidden characters but no whitespace', async function (): Promise<void> {
|
||||
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', ['-e', 'setTimeout(console.log,1000,1+2)']));
|
||||
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
|
||||
expect(exitStatus).eq(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function createTaskConfig(taskType: string, command: string, args: string[]): TaskConfiguration {
|
||||
const options: TaskConfiguration = {
|
||||
label: 'test task',
|
||||
type: taskType,
|
||||
_source: '/source/folder',
|
||||
_scope: '/source/folder',
|
||||
command,
|
||||
args,
|
||||
options: { cwd: wsRoot }
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
function createProcessTaskConfig(processType: ProcessType, command: string, args?: string[], cwd: string = wsRoot): TaskConfiguration {
|
||||
return <ProcessTaskConfiguration>{
|
||||
label: 'test task',
|
||||
type: processType,
|
||||
_source: '/source/folder',
|
||||
_scope: '/source/folder',
|
||||
command,
|
||||
args,
|
||||
options: { cwd },
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function createProcessTaskConfig2(processType: ProcessType, command: string, args?: any[]): TaskConfiguration {
|
||||
return <ProcessTaskConfiguration>{
|
||||
label: 'test task',
|
||||
type: processType,
|
||||
command,
|
||||
args,
|
||||
options: { cwd: wsRoot },
|
||||
};
|
||||
}
|
||||
|
||||
function createTaskConfigTaskLongRunning(processType: ProcessType): TaskConfiguration {
|
||||
return <ProcessTaskConfiguration>{
|
||||
label: '[Task] long running test task (~300s)',
|
||||
type: processType,
|
||||
_source: '/source/folder',
|
||||
_scope: '/source/folder',
|
||||
options: { cwd: wsRoot },
|
||||
command: commandLongRunning,
|
||||
windows: {
|
||||
command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningWindows)),
|
||||
options: { cwd: wsRoot }
|
||||
},
|
||||
osx: {
|
||||
command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningOsx))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function checkSuccessfulProcessExit(taskInfo: TaskInfo, taskWatcher: TaskWatcher): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const toDispose = taskWatcher.onTaskExit((event: TaskExitedEvent) => {
|
||||
if (event.taskId === taskInfo.taskId && event.code === 0) {
|
||||
toDispose.dispose();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getExitStatus(taskInfo: TaskInfo, taskWatcher: TaskWatcher): Promise<string | number> {
|
||||
return new Promise<string | number>((resolve, reject) => {
|
||||
taskWatcher.onTaskExit((event: TaskExitedEvent) => {
|
||||
if (event.taskId === taskInfo.taskId) {
|
||||
if (typeof event.signal === 'string') {
|
||||
resolve(event.signal);
|
||||
} else if (typeof event.code === 'number') {
|
||||
resolve(event.code);
|
||||
} else {
|
||||
reject(new Error('no code nor signal'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
265
packages/task/src/node/task-server.ts
Normal file
265
packages/task/src/node/task-server.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import { Disposable, DisposableCollection, ILogger } from '@theia/core/lib/common/';
|
||||
import {
|
||||
TaskClient,
|
||||
TaskExitedEvent,
|
||||
TaskInfo,
|
||||
TaskServer,
|
||||
TaskConfiguration,
|
||||
TaskOutputProcessedEvent,
|
||||
RunTaskOption,
|
||||
BackgroundTaskEndedEvent
|
||||
} from '../common';
|
||||
import { TaskManager } from './task-manager';
|
||||
import { TaskRunnerRegistry } from './task-runner';
|
||||
import { Task } from './task';
|
||||
import { ProcessTask } from './process/process-task';
|
||||
import { ProblemCollector } from './task-problem-collector';
|
||||
import { CustomTask } from './custom/custom-task';
|
||||
|
||||
@injectable()
|
||||
export class TaskServerImpl implements TaskServer, Disposable {
|
||||
|
||||
/** Task clients, to send notifications-to. */
|
||||
protected clients: TaskClient[] = [];
|
||||
/** Map of task id and task disposable */
|
||||
protected readonly toDispose = new Map<number, DisposableCollection>();
|
||||
|
||||
/** Map of task id and task background status. */
|
||||
// Currently there is only one property ('isActive'), but in the future we may want to store more properties
|
||||
protected readonly backgroundTaskStatusMap = new Map<number, { 'isActive': boolean }>();
|
||||
|
||||
@inject(ILogger) @named('task')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(TaskManager)
|
||||
protected readonly taskManager: TaskManager;
|
||||
|
||||
@inject(TaskRunnerRegistry)
|
||||
protected readonly runnerRegistry: TaskRunnerRegistry;
|
||||
|
||||
/** task context - {task id - problem collector} */
|
||||
private problemCollectors: Map<string, Map<number, ProblemCollector>> = new Map();
|
||||
|
||||
dispose(): void {
|
||||
for (const toDispose of this.toDispose.values()) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
this.toDispose.clear();
|
||||
this.backgroundTaskStatusMap.clear();
|
||||
}
|
||||
|
||||
protected disposeByTaskId(taskId: number): void {
|
||||
if (this.toDispose.has(taskId)) {
|
||||
this.toDispose.get(taskId)!.dispose();
|
||||
this.toDispose.delete(taskId);
|
||||
}
|
||||
|
||||
if (this.backgroundTaskStatusMap.has(taskId)) {
|
||||
this.backgroundTaskStatusMap.delete(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
async getTasks(context?: string): Promise<TaskInfo[]> {
|
||||
const taskInfo: TaskInfo[] = [];
|
||||
const tasks = this.taskManager.getTasks(context);
|
||||
if (tasks !== undefined) {
|
||||
for (const task of tasks) {
|
||||
taskInfo.push(await task.getRuntimeInfo());
|
||||
}
|
||||
}
|
||||
this.logger.debug(`getTasks(): about to return task information for ${taskInfo.length} tasks`);
|
||||
|
||||
return Promise.resolve(taskInfo);
|
||||
}
|
||||
|
||||
async run(taskConfiguration: TaskConfiguration, ctx?: string, option?: RunTaskOption): Promise<TaskInfo> {
|
||||
const runner = taskConfiguration.executionType ?
|
||||
this.runnerRegistry.getRunner(taskConfiguration.type, taskConfiguration.executionType) :
|
||||
this.runnerRegistry.getRunner(taskConfiguration.type);
|
||||
const task = await runner.run(taskConfiguration, ctx);
|
||||
|
||||
if (!this.toDispose.has(task.id)) {
|
||||
this.toDispose.set(task.id, new DisposableCollection());
|
||||
}
|
||||
|
||||
if (taskConfiguration.isBackground && !this.backgroundTaskStatusMap.has(task.id)) {
|
||||
this.backgroundTaskStatusMap.set(task.id, { 'isActive': false });
|
||||
}
|
||||
|
||||
this.toDispose.get(task.id)!.push(
|
||||
task.onExit(event => {
|
||||
this.taskManager.delete(task);
|
||||
this.fireTaskExitedEvent(event, task);
|
||||
this.removedCachedProblemCollector(event.ctx || '', event.taskId);
|
||||
this.disposeByTaskId(event.taskId);
|
||||
})
|
||||
);
|
||||
|
||||
const resolvedMatchers = option && option.customization ? option.customization.problemMatcher || [] : [];
|
||||
if (resolvedMatchers.length > 0) {
|
||||
this.toDispose.get(task.id)!.push(
|
||||
task.onOutput(event => {
|
||||
let collector: ProblemCollector | undefined = this.getCachedProblemCollector(event.ctx || '', event.taskId);
|
||||
if (!collector) {
|
||||
collector = new ProblemCollector(resolvedMatchers);
|
||||
this.cacheProblemCollector(event.ctx || '', event.taskId, collector);
|
||||
}
|
||||
|
||||
const problems = collector.processLine(event.line);
|
||||
if (problems.length > 0) {
|
||||
this.fireTaskOutputProcessedEvent({
|
||||
taskId: event.taskId,
|
||||
config: taskConfiguration,
|
||||
ctx: event.ctx,
|
||||
problems
|
||||
});
|
||||
}
|
||||
if (taskConfiguration.isBackground) {
|
||||
const backgroundTaskStatus = this.backgroundTaskStatusMap.get(event.taskId)!;
|
||||
if (!backgroundTaskStatus.isActive) {
|
||||
// Get the 'activeOnStart' value of the problem matcher 'background' property
|
||||
const activeOnStart = collector.isTaskActiveOnStart();
|
||||
if (activeOnStart) {
|
||||
backgroundTaskStatus.isActive = true;
|
||||
} else {
|
||||
const isBeginsPatternMatch = collector.matchBeginMatcher(event.line);
|
||||
if (isBeginsPatternMatch) {
|
||||
backgroundTaskStatus.isActive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (backgroundTaskStatus.isActive) {
|
||||
const isEndsPatternMatch = collector.matchEndMatcher(event.line);
|
||||
// Mark ends pattern as matches, only after begins pattern matches
|
||||
if (isEndsPatternMatch) {
|
||||
this.fireBackgroundTaskEndedEvent({
|
||||
taskId: event.taskId,
|
||||
ctx: event.ctx
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
this.toDispose.get(task.id)!.push(task);
|
||||
|
||||
const taskInfo = await task.getRuntimeInfo();
|
||||
this.fireTaskCreatedEvent(taskInfo);
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
async getRegisteredTaskTypes(): Promise<string[]> {
|
||||
return this.runnerRegistry.getRunnerTypes();
|
||||
}
|
||||
|
||||
async customExecutionComplete(id: number, exitCode: number | undefined): Promise<void> {
|
||||
const task = this.taskManager.get(id) as CustomTask;
|
||||
await task.callbackTaskComplete(exitCode);
|
||||
}
|
||||
|
||||
protected fireTaskExitedEvent(event: TaskExitedEvent, task?: Task): void {
|
||||
this.logger.debug(log => log('task has exited:', event));
|
||||
|
||||
if (task instanceof ProcessTask) {
|
||||
this.clients.forEach(client => {
|
||||
client.onDidEndTaskProcess(event);
|
||||
});
|
||||
}
|
||||
|
||||
this.clients.forEach(client => {
|
||||
client.onTaskExit(event);
|
||||
});
|
||||
}
|
||||
|
||||
protected fireTaskCreatedEvent(event: TaskInfo, task?: Task): void {
|
||||
this.logger.debug(log => log('task created:', event));
|
||||
|
||||
this.clients.forEach(client => {
|
||||
client.onTaskCreated(event);
|
||||
});
|
||||
|
||||
if (task && task instanceof ProcessTask) {
|
||||
this.clients.forEach(client => {
|
||||
client.onDidStartTaskProcess(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected fireTaskOutputProcessedEvent(event: TaskOutputProcessedEvent): void {
|
||||
this.clients.forEach(client => client.onDidProcessTaskOutput(event));
|
||||
}
|
||||
|
||||
protected fireBackgroundTaskEndedEvent(event: BackgroundTaskEndedEvent): void {
|
||||
this.clients.forEach(client => client.onBackgroundTaskEnded(event));
|
||||
}
|
||||
|
||||
/** Kill task for a given id. Rejects if task is not found */
|
||||
async kill(id: number): Promise<void> {
|
||||
const taskToKill = this.taskManager.get(id);
|
||||
if (taskToKill !== undefined) {
|
||||
this.logger.info(`Killing task id ${id}`);
|
||||
return taskToKill.kill();
|
||||
} else {
|
||||
this.logger.info(`Could not find task to kill, task id ${id}. Already terminated?`);
|
||||
return Promise.reject(new Error(`Could not find task to kill, task id ${id}. Already terminated?`));
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds a client to this server */
|
||||
setClient(client: TaskClient): void {
|
||||
this.logger.debug('a client has connected - adding it to the list:');
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
||||
/** Removes a client, from this server */
|
||||
disconnectClient(client: TaskClient): void {
|
||||
this.logger.debug('a client has disconnected - removed from list:');
|
||||
const idx = this.clients.indexOf(client);
|
||||
if (idx > -1) {
|
||||
this.clients.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private getCachedProblemCollector(ctx: string, taskId: number): ProblemCollector | undefined {
|
||||
if (this.problemCollectors.has(ctx)) {
|
||||
return this.problemCollectors.get(ctx)!.get(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
private cacheProblemCollector(ctx: string, taskId: number, problemCollector: ProblemCollector): void {
|
||||
if (this.problemCollectors.has(ctx)) {
|
||||
if (!this.problemCollectors.get(ctx)!.has(taskId)) {
|
||||
this.problemCollectors.get(ctx)!.set(taskId, problemCollector);
|
||||
}
|
||||
} else {
|
||||
const forNewContext = new Map<number, ProblemCollector>();
|
||||
forNewContext.set(taskId, problemCollector);
|
||||
this.problemCollectors.set(ctx, forNewContext);
|
||||
}
|
||||
}
|
||||
|
||||
private removedCachedProblemCollector(ctx: string, taskId: number): void {
|
||||
if (this.problemCollectors.has(ctx) && this.problemCollectors.get(ctx)!.has(taskId)) {
|
||||
this.problemCollectors.get(ctx)!.delete(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
packages/task/src/node/task.ts
Normal file
103
packages/task/src/node/task.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, unmanaged } from '@theia/core/shared/inversify';
|
||||
import { ILogger, Disposable, DisposableCollection, Emitter, Event, MaybePromise } from '@theia/core/lib/common/';
|
||||
import { TaskInfo, TaskExitedEvent, TaskConfiguration, TaskOutputEvent, ManagedTask, ManagedTaskManager } from '../common/task-protocol';
|
||||
/**
|
||||
* Represents the options used for running a task.
|
||||
*/
|
||||
export interface TaskOptions {
|
||||
/** The task label */
|
||||
label: string;
|
||||
/** The task configuration which should be executed */
|
||||
config: TaskConfiguration;
|
||||
/** The optional execution context */
|
||||
context?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Task} represents the execution state of a `TaskConfiguration`.
|
||||
* Implementing classes have to call the {@link Task#fireOutputLine} function
|
||||
* whenever a new output occurs during the execution.
|
||||
*/
|
||||
@injectable()
|
||||
export abstract class Task implements Disposable, ManagedTask {
|
||||
|
||||
protected taskId: number;
|
||||
protected readonly toDispose: DisposableCollection = new DisposableCollection();
|
||||
readonly exitEmitter: Emitter<TaskExitedEvent>;
|
||||
readonly outputEmitter: Emitter<TaskOutputEvent>;
|
||||
|
||||
constructor(
|
||||
@unmanaged() protected readonly taskManager: ManagedTaskManager<Task>,
|
||||
@unmanaged() protected readonly logger: ILogger,
|
||||
@unmanaged() protected readonly options: TaskOptions
|
||||
) {
|
||||
this.taskId = this.taskManager.register(this, this.options.context);
|
||||
this.exitEmitter = new Emitter<TaskExitedEvent>();
|
||||
this.outputEmitter = new Emitter<TaskOutputEvent>();
|
||||
this.toDispose.push(this.exitEmitter);
|
||||
this.toDispose.push(this.outputEmitter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate this task.
|
||||
*
|
||||
* @returns a promise that resolves once the task has been properly terminated.
|
||||
*/
|
||||
abstract kill(): Promise<void>;
|
||||
|
||||
get onExit(): Event<TaskExitedEvent> {
|
||||
return this.exitEmitter.event;
|
||||
}
|
||||
|
||||
get onOutput(): Event<TaskOutputEvent> {
|
||||
return this.outputEmitter.event;
|
||||
}
|
||||
|
||||
/** Has to be called when a task has concluded its execution. */
|
||||
protected fireTaskExited(event: TaskExitedEvent): void {
|
||||
this.exitEmitter.fire(event);
|
||||
}
|
||||
|
||||
protected fireOutputLine(event: TaskOutputEvent): void {
|
||||
this.outputEmitter.fire(event);
|
||||
}
|
||||
/**
|
||||
* Retrieves the runtime information about this task.
|
||||
* The runtime information computation may happen asynchronous.
|
||||
*
|
||||
* @returns (a promise of) the runtime information as `TaskInfo`.
|
||||
*/
|
||||
abstract getRuntimeInfo(): MaybePromise<TaskInfo>;
|
||||
|
||||
get id(): number {
|
||||
return this.taskId;
|
||||
}
|
||||
|
||||
get context(): string | undefined {
|
||||
return this.options.context;
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this.options.label;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
}
|
||||
63
packages/task/src/node/test/task-test-container.ts
Normal file
63
packages/task/src/node/test/task-test-container.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson 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-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { Container } from '@theia/core/shared/inversify';
|
||||
import { bindLogger } from '@theia/core/lib/node/logger-backend-module';
|
||||
import { backendApplicationModule } from '@theia/core/lib/node/backend-application-module';
|
||||
import processBackendModule from '@theia/process/lib/node/process-backend-module';
|
||||
import terminalBackendModule from '@theia/terminal/lib/node/terminal-backend-module';
|
||||
import taskBackendModule from '../task-backend-module';
|
||||
import filesystemBackendModule from '@theia/filesystem/lib/node/filesystem-backend-module';
|
||||
import workspaceServer from '@theia/workspace/lib/node/workspace-backend-module';
|
||||
import { messagingBackendModule } from '@theia/core/lib/node/messaging/messaging-backend-module';
|
||||
import { ApplicationPackage } from '@theia/core/shared/@theia/application-package';
|
||||
import { TerminalProcess } from '@theia/process/lib/node';
|
||||
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
|
||||
|
||||
export function createTaskTestContainer(): Container {
|
||||
const testContainer = new Container();
|
||||
|
||||
testContainer.load(backendApplicationModule);
|
||||
testContainer.rebind(ApplicationPackage).toConstantValue({} as ApplicationPackage);
|
||||
|
||||
bindLogger(testContainer.bind.bind(testContainer));
|
||||
testContainer.load(messagingBackendModule);
|
||||
testContainer.load(processBackendModule);
|
||||
testContainer.load(taskBackendModule);
|
||||
testContainer.load(filesystemBackendModule);
|
||||
testContainer.load(workspaceServer);
|
||||
testContainer.load(terminalBackendModule);
|
||||
|
||||
// Make it easier to debug processes.
|
||||
testContainer.rebind(TerminalProcess).to(TestTerminalProcess);
|
||||
|
||||
testContainer.rebind(ProcessUtils).toConstantValue(new class extends ProcessUtils {
|
||||
override terminateProcessTree(): void { } // don't actually kill the tree, it breaks the tests.
|
||||
});
|
||||
|
||||
return testContainer;
|
||||
}
|
||||
|
||||
class TestTerminalProcess extends TerminalProcess {
|
||||
|
||||
protected override emitOnStarted(): void {
|
||||
if (process.env['THEIA_TASK_TEST_DEBUG']) {
|
||||
console.log(`START ${this.id} ${JSON.stringify([this.executable, this.options.commandLine, ...this.arguments])}`);
|
||||
this.outputStream.on('data', data => console.debug(`${this.id} OUTPUT: ${data.toString().trim()}`));
|
||||
}
|
||||
super.emitOnStarted();
|
||||
}
|
||||
|
||||
}
|
||||
60
packages/task/test-resources/.theia/tasks.json
Normal file
60
packages/task/test-resources/.theia/tasks.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
// comment
|
||||
"tasks": [
|
||||
{
|
||||
"label": "test task",
|
||||
"type": "shell",
|
||||
"command": "./task",
|
||||
"args": [
|
||||
"test"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"task.bat",
|
||||
"abc"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "long running test task",
|
||||
"type": "shell",
|
||||
"command": "./task-long-running",
|
||||
"args": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"task-long-running.bat",
|
||||
"abc"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "list all files",
|
||||
"type": "shell",
|
||||
"command": "ls",
|
||||
"args": [
|
||||
"-alR",
|
||||
"/"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmd.exe",
|
||||
"args": [
|
||||
"/c",
|
||||
"dir"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
packages/task/test-resources/compare.js
Normal file
14
packages/task/test-resources/compare.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Compares if two arrays contain the same primitive values.
|
||||
*/
|
||||
exports.compareArrayValues = function (a, b) {
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
8
packages/task/test-resources/task
Executable file
8
packages/task/test-resources/task
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
for i in $@
|
||||
do
|
||||
sleep 1
|
||||
echo "tasking... $i"
|
||||
done
|
||||
|
||||
8
packages/task/test-resources/task-long-running
Executable file
8
packages/task/test-resources/task-long-running
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
for i in {1..300}
|
||||
do
|
||||
sleep 1
|
||||
echo "tasking... $i"
|
||||
done
|
||||
|
||||
7
packages/task/test-resources/task-long-running-osx
Executable file
7
packages/task/test-resources/task-long-running-osx
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
for i in {1..300}
|
||||
do
|
||||
sleep 1
|
||||
echo "tasking osx... $i"
|
||||
done
|
||||
10
packages/task/test-resources/task-long-running.bat
Normal file
10
packages/task/test-resources/task-long-running.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
|
||||
@echo off
|
||||
for /l %%x in (1,1,300) do (
|
||||
echo tasking... %%x
|
||||
@REM sleep for ~1s
|
||||
@REM see: https://stackoverflow.com/questions/735285/how-to-wait-in-a-batch-script
|
||||
ping 192.0.2.2 -n 1 -w 1000> nul
|
||||
)
|
||||
echo "done"
|
||||
7
packages/task/test-resources/task-osx
Executable file
7
packages/task/test-resources/task-osx
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
for i in $@
|
||||
do
|
||||
sleep 1
|
||||
echo "tasking osx... $i"
|
||||
done
|
||||
8
packages/task/test-resources/task.bat
Normal file
8
packages/task/test-resources/task.bat
Normal file
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
|
||||
for /l %%x in (1,1,3) do (
|
||||
echo tasking... %*
|
||||
@REM sleep for ~1s
|
||||
waitfor nothing /t 1 > nul
|
||||
)
|
||||
echo "done"
|
||||
13
packages/task/test-resources/test-arguments-0.js
Normal file
13
packages/task/test-resources/test-arguments-0.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const {
|
||||
compareArrayValues
|
||||
} = require('./compare')
|
||||
|
||||
const debugHint = process.argv[2]
|
||||
const args = process.argv.slice(3)
|
||||
|
||||
if (compareArrayValues(args, ['a', 'b', 'c'])) {
|
||||
process.exit(0) // OK
|
||||
} else {
|
||||
console.error(debugHint, JSON.stringify(args))
|
||||
process.exit(1) // NOT OK
|
||||
}
|
||||
13
packages/task/test-resources/test-arguments-1.js
Normal file
13
packages/task/test-resources/test-arguments-1.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const {
|
||||
compareArrayValues
|
||||
} = require('./compare')
|
||||
|
||||
const debugHint = process.argv[2]
|
||||
const args = process.argv.slice(3)
|
||||
|
||||
if (compareArrayValues(args, ['a', 'b', ' c'])) {
|
||||
process.exit(0) // OK
|
||||
} else {
|
||||
console.error(debugHint, JSON.stringify(args))
|
||||
process.exit(1) // NOT OK
|
||||
}
|
||||
13
packages/task/test-resources/test-arguments-2.js
Normal file
13
packages/task/test-resources/test-arguments-2.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const {
|
||||
compareArrayValues
|
||||
} = require('./compare')
|
||||
|
||||
const debugHint = process.argv[2]
|
||||
const args = process.argv.slice(3)
|
||||
|
||||
if (compareArrayValues(args, ['a', 'b', 'c"'])) {
|
||||
process.exit(0) // OK
|
||||
} else {
|
||||
console.error(debugHint, JSON.stringify(args))
|
||||
process.exit(1) // NOT OK
|
||||
}
|
||||
43
packages/task/tsconfig.json
Normal file
43
packages/task/tsconfig.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../editor"
|
||||
},
|
||||
{
|
||||
"path": "../filesystem"
|
||||
},
|
||||
{
|
||||
"path": "../markers"
|
||||
},
|
||||
{
|
||||
"path": "../monaco"
|
||||
},
|
||||
{
|
||||
"path": "../process"
|
||||
},
|
||||
{
|
||||
"path": "../terminal"
|
||||
},
|
||||
{
|
||||
"path": "../userstorage"
|
||||
},
|
||||
{
|
||||
"path": "../variable-resolver"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user