Skip to content

Commit

Permalink
support "group" in the task config
Browse files Browse the repository at this point in the history
- With this change users would be able to define and run build tasks,
test tasks, default build tasks, and default test tasks.
- resolves #6144

Signed-off-by: Liang Huang <liang.huang@ericsson.com>
  • Loading branch information
Liang Huang committed Nov 2, 2019
1 parent fad9056 commit c4c3017
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 15 deletions.
95 changes: 90 additions & 5 deletions packages/task/src/browser/quick-open-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { inject, injectable } from 'inversify';
import { TaskService } from './task-service';
import { TaskInfo, TaskConfiguration } from '../common/task-protocol';
import { TaskInfo, TaskConfiguration, TaskCustomization } from '../common/task-protocol';
import { TaskDefinitionRegistry } from './task-definition-registry';
import URI from '@theia/core/lib/common/uri';
import { TaskActionProvider } from './task-action-provider';
Expand Down Expand Up @@ -259,6 +259,70 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler {
});
}

async runBuildOrTestTask(buildOrTestType: 'build' | 'test'): Promise<void> {
const shouldRunBuildTask = buildOrTestType === 'build';
await this.init();
const buildOrTestTasks = this.items.filter((t: TaskRunQuickOpenItem) =>
shouldRunBuildTask ? TaskCustomization.isBuildTask(t.getTask()) : TaskCustomization.isTestTask(t.getTask())
);
if (buildOrTestTasks.length > 0) { // build / test tasks are defined in the workspace
const defaultBuildOrTestTask = buildOrTestTasks.find((t: TaskRunQuickOpenItem) =>
shouldRunBuildTask ? TaskCustomization.isDefaultBuildTask(t.getTask()) : TaskCustomization.isDefaultTestTask(t.getTask())
);
if (defaultBuildOrTestTask) { // run the default build / test task
const taskToRun = (defaultBuildOrTestTask as TaskRunQuickOpenItem).getTask();
if (this.taskDefinitionRegistry && !!this.taskDefinitionRegistry.getDefinition(taskToRun)) {
this.taskService.run(taskToRun.source, taskToRun.label);
} else {
this.taskService.run(taskToRun._source, taskToRun.label);
}
return;
}

// if default build / test task is not found, 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 = [new QuickOpenItem({
label: `No ${buildOrTestType} task to run found. Configure ${buildOrTestType} task...`,
run: (mode: QuickOpenMode): boolean => {
if (mode !== QuickOpenMode.OPEN) {
return false;
}

this.init().then(() => {
// update the `tasks.json` file, instead of running the task itself
this.items = this.items.map((item: TaskRunQuickOpenItem) => {
const newItem = new ConfigureBuildOrTestTaskQuickOpenItem(
item.getTask(),
this.taskService,
this.workspaceService.isMultiRootWorkspaceOpened,
item.options,
this.taskNameResolver,
shouldRunBuildTask
);
newItem['taskDefinitionRegistry'] = this.taskDefinitionRegistry;
return newItem;
});
this.quickOpenService.open(this, {
placeholder: `Select the task to be used as the default ${buildOrTestType} task`,
fuzzyMatchLabel: true,
fuzzySort: false
});
});

return true;
}
})];
}

this.quickOpenService.open(this, {
placeholder: `Select the ${buildOrTestType} task to run`,
fuzzyMatchLabel: true,
fuzzySort: false
});
}

onType(lookFor: string, acceptor: (items: QuickOpenItem[], actionProvider?: QuickOpenActionProvider) => void): void {
acceptor(this.items, this.actionProvider);
}
Expand All @@ -272,9 +336,9 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler {

const filteredRecentTasks: TaskConfiguration[] = [];
recentTasks.forEach(recent => {
const exist = [...configuredTasks, ...providedTasks].some(t => this.taskDefinitionRegistry.compareTasks(recent, t));
if (exist) {
filteredRecentTasks.push(recent);
const originalTaskConfig = [...configuredTasks, ...providedTasks].find(t => this.taskDefinitionRegistry.compareTasks(recent, t));
if (originalTaskConfig) {
filteredRecentTasks.push(originalTaskConfig);
}
});

Expand Down Expand Up @@ -324,7 +388,7 @@ export class TaskRunQuickOpenItem extends QuickOpenGroupItem {
protected readonly task: TaskConfiguration,
protected taskService: TaskService,
protected isMulti: boolean,
protected readonly options: QuickOpenGroupItemOptions,
public readonly options: QuickOpenGroupItemOptions,
protected readonly taskNameResolver: TaskNameResolver,
) {
super(options);
Expand Down Expand Up @@ -371,6 +435,27 @@ export class TaskRunQuickOpenItem extends QuickOpenGroupItem {
}
}

export class ConfigureBuildOrTestTaskQuickOpenItem extends TaskRunQuickOpenItem {
constructor(
protected readonly task: TaskConfiguration,
protected taskService: TaskService,
protected isMulti: boolean,
public readonly options: QuickOpenGroupItemOptions,
protected readonly taskNameResolver: TaskNameResolver,
protected readonly isBuildTask: boolean
) {
super(task, taskService, isMulti, options, taskNameResolver);
}

run(mode: QuickOpenMode): boolean {
if (mode !== QuickOpenMode.OPEN) {
return false;
}
this.taskService.updateTaskConfiguration(this.task, { group: { kind: this.isBuildTask ? 'build' : 'test', isDefault: true } });
return true;
}
}

export class TaskAttachQuickOpenItem extends QuickOpenItem {

constructor(
Expand Down
17 changes: 11 additions & 6 deletions packages/task/src/browser/task-configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,14 @@ export class TaskConfigurations implements Disposable {
}

/**
* saves the names of the problem matchers to be used to parse the output of the given task to `tasks.json`
* @param task task that the problem matcher(s) are applied to
* @param problemMatchers name(s) of the problem matcher(s)
* 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 appplied
*/
async saveProblemMatcherForTask(task: TaskConfiguration, problemMatchers: string[]): Promise<void> {
// tslint:disable-next-line:no-any
async updateTaskConfig(task: TaskConfiguration, update: { [name: string]: any }): Promise<void> {
const sourceFolderUri: string | undefined = this.getSourceFolderUriFromTask(task);
if (!sourceFolderUri) {
console.error('Global task cannot be customized');
Expand All @@ -396,12 +399,14 @@ export class TaskConfigurations implements Disposable {
});
jsonTasks[ind] = {
...jsonTasks[ind],
problemMatcher: problemMatchers.map(name => name.startsWith('$') ? name : `$${name}`)
...update
};
}
this.taskConfigurationManager.setTaskConfigurations(sourceFolderUri, jsonTasks);
} else { // task is not in `tasks.json`
task.problemMatcher = problemMatchers;
Object.keys(update).forEach(taskProperty => {
task[taskProperty] = update[taskProperty];
});
this.saveTask(sourceFolderUri, task);
}
}
Expand Down
50 changes: 47 additions & 3 deletions packages/task/src/browser/task-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-con
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';
Expand All @@ -38,6 +39,18 @@ export namespace TaskCommands {
label: 'Run Task...'
};

export const TASK_RUN_BUILD: Command = {
id: 'task:run:build',
category: TASK_CATEGORY,
label: 'Run Build Task...'
};

export const TASK_RUN_TEST: Command = {
id: 'task:run:test',
category: TASK_CATEGORY,
label: 'Run Test Task...'
};

export const WORKBENCH_RUN_TASK: Command = {
id: 'workbench.action.tasks.runTask',
category: TASK_CATEGORY
Expand Down Expand Up @@ -135,6 +148,9 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri
@inject(StatusBar)
protected readonly statusBar: StatusBar;

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

@postConstruct()
protected async init(): Promise<void> {
this.taskWatcher.onTaskCreated(() => this.updateRunningTasksItem());
Expand Down Expand Up @@ -209,6 +225,24 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri
}
}
);
registry.registerCommand(
TaskCommands.TASK_RUN_BUILD,
{
isEnabled: () => this.workspaceService.opened,
// tslint:disable-next-line:no-any
execute: (...args: any[]) =>
this.quickOpenTask.runBuildOrTestTask('build')
}
);
registry.registerCommand(
TaskCommands.TASK_RUN_TEST,
{
isEnabled: () => this.workspaceService.opened,
// tslint:disable-next-line:no-any
execute: (...args: any[]) =>
this.quickOpenTask.runBuildOrTestTask('test')
}
);
registry.registerCommand(
TaskCommands.TASK_ATTACH,
{
Expand Down Expand Up @@ -268,20 +302,30 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri
});

menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
commandId: TaskCommands.TASK_RUN_LAST.id,
commandId: TaskCommands.TASK_RUN_BUILD.id,
order: '1'
});

menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
commandId: TaskCommands.TASK_ATTACH.id,
commandId: TaskCommands.TASK_RUN_TEST.id,
order: '2'
});

menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, {
commandId: TaskCommands.TASK_RUN_TEXT.id,
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: 'Show Running Tasks...',
Expand Down
38 changes: 38 additions & 0 deletions packages/task/src/browser/task-schema-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,44 @@ const taskConfigurationSchema: IJSONSchema = {
},
command: commandSchema,
args: commandArgSchema,
group: {
oneOf: [
{
type: 'string'
},
{
type: 'object',
properties: {
kind: {
type: 'string',
default: 'none',
description: 'The task\'s execution group.'
},
isDefault: {
type: 'boolean',
default: false,
description: 'Defines if this task is the default task in the group.'
}
}
}
],
enum: [
{ kind: 'build', isDefault: true },
{ kind: 'test', isDefault: true },
'build',
'test',
'none'
],
enumDescriptions: [
'Marks the task as the default build task.',
'Marks the task as the default test task.',
'Marks the task as a build task accessible through the \'Run Build Task\' command.',
'Marks the task as a test task accessible through the \'Run Test Task\' command.',
'Assigns the task to no group'
],
// tslint:disable-next-line:max-line-length
description: '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.'
},
options: commandOptionsSchema,
windows: {
type: 'object',
Expand Down
25 changes: 24 additions & 1 deletion packages/task/src/browser/task-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ export class TaskService implements TaskConfigurationClient {
customizationObject.problemMatcher = matcherNames;

// write the selected matcher (or the decision of "never parse") into the `tasks.json`
this.taskConfigurations.saveProblemMatcherForTask(task, matcherNames);
this.updateTaskConfiguration(task, { problemMatcher: matcherNames });
} else if (selected.learnMore) { // user wants to learn more about parsing task output
open(this.openerService, new URI('https://code.visualstudio.com/docs/editor/tasks#_processing-task-output-with-problem-matchers'));
}
Expand Down Expand Up @@ -413,6 +413,29 @@ export class TaskService implements TaskConfigurationClient {
});
}

/**
* Updates the task configuration 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 appplied
*/
// tslint:disable-next-line:no-any
async updateTaskConfiguration(task: TaskConfiguration, update: { [name: string]: any }): Promise<void> {
if (update.problemMatcher) {
if (Array.isArray(update.problemMatcher)) {
update.problemMatcher.forEach((name, index) => {
if (!name.startsWith('$')) {
update.problemMatcher[index] = `$${update.problemMatcher[index]}`;
}
});
} else if (!update.problemMatcher.startsWith('$')) {
update.problemMatcher = `$${update.problemMatcher}`;
}
}
this.taskConfigurations.updateTaskConfig(task, update);
}

protected async getWorkspaceTasks(workspaceFolderUri: string | undefined): Promise<TaskConfiguration[]> {
const tasks = await this.getTasks();
return tasks.filter(t => t._scope === workspaceFolderUri || t._scope === undefined);
Expand Down
18 changes: 18 additions & 0 deletions packages/task/src/common/task-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,28 @@ export const TaskClient = Symbol('TaskClient');

export interface TaskCustomization {
type: string;
group?: 'build' | 'test' | 'none' | { kind: 'build' | 'test' | 'none', isDefault: true };
problemMatcher?: string | ProblemMatcherContribution | (string | ProblemMatcherContribution)[];
// tslint:disable-next-line:no-any
[name: string]: any;
}
export namespace TaskCustomization {
export function isBuildTask(task: TaskCustomization): boolean {
return task.group === 'build' || !!task.group && typeof task.group === 'object' && task.group.kind === 'build';
}

export function isDefaultBuildTask(task: TaskCustomization): boolean {
return !!task.group && typeof task.group === 'object' && task.group.kind === 'build' && task.group.isDefault;
}

export function isTestTask(task: TaskCustomization): boolean {
return task.group === 'test' || !!task.group && typeof task.group === 'object' && task.group.kind === 'test';
}

export function isDefaultTestTask(task: TaskCustomization): boolean {
return !!task.group && typeof task.group === 'object' && task.group.kind === 'test' && task.group.isDefault;
}
}

export interface TaskConfiguration extends TaskCustomization {
/** A label that uniquely identifies a task configuration per source */
Expand Down

0 comments on commit c4c3017

Please sign in to comment.