diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index b5293a49b063d..ee17b332d8018 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -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'; @@ -259,6 +259,78 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { }); } + async runBuildOrTestTask(buildOrTestType: 'build' | 'test'): Promise { + const shouldRunBuildTask = buildOrTestType === 'build'; + await this.init(); + if (this.items.length > 1 || + this.items.length === 1 && (this.items[0] as TaskRunQuickOpenItem).getTask !== undefined) { // the item in `this.items` is not 'No tasks found' + + const buildOrTestTasks = this.items.filter((t: TaskRunQuickOpenItem) => + shouldRunBuildTask ? TaskCustomization.isBuildTask(t.getTask()) : TaskCustomization.isTestTask(t.getTask()) + ); + this.actionProvider = undefined; + if (buildOrTestTasks.length > 0) { // build / test tasks are defined in the workspace + const defaultBuildOrTestTasks = buildOrTestTasks.filter((t: TaskRunQuickOpenItem) => + shouldRunBuildTask ? TaskCustomization.isDefaultBuildTask(t.getTask()) : TaskCustomization.isDefaultTestTask(t.getTask()) + ); + if (defaultBuildOrTestTasks.length === 1) { // run the default build / test task + const defaultBuildOrTestTask = defaultBuildOrTestTasks[0]; + 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, 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 = [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, + this.taskConfigurationManager + ); + 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); } @@ -272,9 +344,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); } }); @@ -324,7 +396,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); @@ -371,6 +443,33 @@ 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, + protected taskConfigurationManager: TaskConfigurationManager + ) { + 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 } }) + .then(() => { + if (this.task._scope) { + this.taskConfigurationManager.openConfiguration(this.task._scope); + } + }); + return true; + } +} + export class TaskAttachQuickOpenItem extends QuickOpenItem { constructor( diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index f98b090c93b1b..074a4d1a533e0 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -136,7 +136,7 @@ export class TaskConfigurations implements Disposable { for (const cus of customizations) { const detected = await this.providedTaskConfigurations.getTaskToCustomize(cus, rootFolder); if (detected) { - detectedTasksAsConfigured.push(detected); + detectedTasksAsConfigured.push({ ...detected, ...cus }); } } } @@ -280,7 +280,7 @@ export class TaskConfigurations implements Disposable { const configuredAndCustomizedTasks = await this.getTasks(); if (!configuredAndCustomizedTasks.some(t => this.taskDefinitionRegistry.compareTasks(t, task))) { - await this.saveTask(sourceFolderUri, task); + await this.saveTask(sourceFolderUri, { ...task, problemMatcher: [] }); } try { @@ -302,8 +302,8 @@ export class TaskConfigurations implements Disposable { customization[p] = task[p]; } }); - const problemMatcher: string[] = []; - if (task.problemMatcher) { + if ('problemMatcher' in task) { + const problemMatcher: string[] = []; if (Array.isArray(task.problemMatcher)) { problemMatcher.push(...task.problemMatcher.map(t => { if (typeof t === 'string') { @@ -314,14 +314,15 @@ export class TaskConfigurations implements Disposable { })); } else if (typeof task.problemMatcher === 'string') { problemMatcher.push(task.problemMatcher); - } else { + } else if (task.problemMatcher) { problemMatcher.push(task.problemMatcher.name!); } + customization.problemMatcher = problemMatcher.map(name => name.startsWith('$') ? name : `$${name}`); } - return { - ...customization, - problemMatcher: problemMatcher.map(name => name.startsWith('$') ? name : `$${name}`) - }; + if (task.group) { + customization.group = task.group; + } + return { ...customization }; } /** Writes the task to a config file. Creates a config file if this one does not exist */ @@ -370,11 +371,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 { + // tslint:disable-next-line:no-any + async updateTaskConfig(task: TaskConfiguration, update: { [name: string]: any }): Promise { const sourceFolderUri: string | undefined = this.getSourceFolderUriFromTask(task); if (!sourceFolderUri) { console.error('Global task cannot be customized'); @@ -396,12 +400,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); } } diff --git a/packages/task/src/browser/task-frontend-contribution.ts b/packages/task/src/browser/task-frontend-contribution.ts index 12a6add725670..ff53f78064fcb 100644 --- a/packages/task/src/browser/task-frontend-contribution.ts +++ b/packages/task/src/browser/task-frontend-contribution.ts @@ -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'; @@ -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 @@ -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 { this.taskWatcher.onTaskCreated(() => this.updateRunningTasksItem()); @@ -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, { @@ -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...', diff --git a/packages/task/src/browser/task-schema-updater.ts b/packages/task/src/browser/task-schema-updater.ts index b65316f1200a8..75bec2da84070 100644 --- a/packages/task/src/browser/task-schema-updater.ts +++ b/packages/task/src/browser/task-schema-updater.ts @@ -99,6 +99,7 @@ export class TaskSchemaUpdater { }); customizedDetectedTask.properties!.problemMatcher = problemMatcher; customizedDetectedTask.properties!.options = commandOptionsSchema; + customizedDetectedTask.properties!.group = group; customizedDetectedTasks.push(customizedDetectedTask); }); @@ -227,6 +228,44 @@ const problemMatcher = { } ] }; +const 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.' +}; const processTaskConfigurationSchema: IJSONSchema = { type: 'object', @@ -250,6 +289,7 @@ const processTaskConfigurationSchema: IJSONSchema = { description: 'Linux specific command configuration that overrides the default command, args, and options', properties: commandAndArgs }, + group, problemMatcher } }; diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index 9d692b0a391bb..72a9afced7122 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -354,7 +354,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')); } @@ -417,6 +417,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 { + 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 { const tasks = await this.getTasks(); return tasks.filter(t => t._scope === workspaceFolderUri || t._scope === undefined); diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index ac8d4cd2432e8..a131f3fa03429 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -25,10 +25,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 */