diff --git a/packages/task/src/browser/provided-task-configurations.spec.ts b/packages/task/src/browser/provided-task-configurations.spec.ts index 6849cc8257c5b..151155bd3e0ca 100644 --- a/packages/task/src/browser/provided-task-configurations.spec.ts +++ b/packages/task/src/browser/provided-task-configurations.spec.ts @@ -17,6 +17,7 @@ import { assert } from 'chai'; import { Container } from 'inversify'; import { ProvidedTaskConfigurations } from './provided-task-configurations'; +import { TaskDefinitionRegistry } from './task-definition-registry'; import { TaskProviderRegistry } from './task-contribution'; import { TaskConfiguration } from '../common'; @@ -26,6 +27,7 @@ describe('provided-task-configurations', () => { container = new Container(); container.bind(ProvidedTaskConfigurations).toSelf().inSingletonScope(); container.bind(TaskProviderRegistry).toSelf().inSingletonScope(); + container.bind(TaskDefinitionRegistry).toSelf().inSingletonScope(); }); it('provided-task-search', async () => { diff --git a/packages/task/src/browser/provided-task-configurations.ts b/packages/task/src/browser/provided-task-configurations.ts index 2369b11060fee..11dfee12c2432 100644 --- a/packages/task/src/browser/provided-task-configurations.ts +++ b/packages/task/src/browser/provided-task-configurations.ts @@ -15,8 +15,10 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { TaskConfiguration } from '../common/task-protocol'; import { TaskProviderRegistry } from './task-contribution'; +import { TaskDefinitionRegistry } from './task-definition-registry'; +import { TaskConfiguration, TaskCustomization } from '../common'; +import URI from '@theia/core/lib/common/uri'; @injectable() export class ProvidedTaskConfigurations { @@ -30,13 +32,14 @@ export class ProvidedTaskConfigurations { @inject(TaskProviderRegistry) protected readonly taskProviderRegistry: TaskProviderRegistry; + @inject(TaskDefinitionRegistry) + protected readonly taskDefinitionRegistry: TaskDefinitionRegistry; + /** returns a list of provided tasks */ async getTasks(): Promise { - const providedTasks: TaskConfiguration[] = []; const providers = this.taskProviderRegistry.getProviders(); - for (const provider of providers) { - providedTasks.push(...await provider.provideTasks()); - } + const providedTasks: TaskConfiguration[] = (await Promise.all(providers.map(p => p.provideTasks()))) + .reduce((acc, taskArray) => acc.concat(taskArray), []); this.cacheTasks(providedTasks); return providedTasks; } @@ -52,6 +55,51 @@ export class ProvidedTaskConfigurations { } } + /** + * 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(customization: TaskCustomization, rootFolderPath: string): Promise { + const definition = this.taskDefinitionRegistry.getDefinition(customization); + if (!definition) { + return undefined; + } + + const matchedTasks: TaskConfiguration[] = []; + let highest = -1; + const tasks = await this.getTasks(); + for (const task of tasks) { // find detected tasks that match the `definition` + let score = 0; + if (!definition.properties.required.every(requiredProp => customization[requiredProp] !== undefined)) { + continue; + } + score += definition.properties.required.length; // number of required properties + const requiredProps = new Set(definition.properties.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); + } + } + + // 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 rootFolderUri = new URI(rootFolderPath).toString(); + const matchedTask = matchedTasks.filter(t => + rootFolderUri === t._scope && definition.properties.all.every(p => t[p] === customization[p]) + )[0]; + return matchedTask; + } + protected getCachedTask(source: string, taskLabel: string): TaskConfiguration | undefined { const labelConfigMap = this.tasksMap.get(source); if (labelConfigMap) { diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index cd9c7ef6129c7..e5eae470680d8 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -20,7 +20,7 @@ import { QuickOpenGroupItem, QuickOpenMode, QuickOpenHandler, QuickOpenOptions, QuickOpenActionProvider, QuickOpenGroupItemOptions } from '@theia/core/lib/browser/quick-open/'; import { TaskService } from './task-service'; -import { TaskInfo, TaskConfiguration } from '../common/task-protocol'; +import { ContributedTaskConfiguration, TaskInfo, TaskConfiguration } from '../common/task-protocol'; import { TaskConfigurations } from './task-configurations'; import { TaskDefinitionRegistry } from './task-definition-registry'; import URI from '@theia/core/lib/common/uri'; @@ -66,7 +66,7 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { /** Initialize this quick open model with the tasks. */ async init(): Promise { const recentTasks = this.taskService.recentTasks; - const configuredTasks = this.taskService.getConfiguredTasks(); + const configuredTasks = await this.taskService.getConfiguredTasks(); const providedTasks = await this.taskService.getProvidedTasks(); const { filteredRecentTasks, filteredConfiguredTasks, filteredProvidedTasks } = this.getFilteredTasks(recentTasks, configuredTasks, providedTasks); @@ -213,7 +213,7 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { const filteredProvidedTasks: TaskConfiguration[] = []; providedTasks.forEach(provided => { - const exist = [...filteredRecentTasks, ...configuredTasks].some(t => TaskConfiguration.equals(provided, t)); + const exist = [...filteredRecentTasks, ...configuredTasks].some(t => ContributedTaskConfiguration.equals(provided, t)); if (!exist) { filteredProvidedTasks.push(provided); } diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index 4dd87cec2ad67..764ad4b06ed4e 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -15,8 +15,9 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { ContributedTaskConfiguration, TaskConfiguration, TaskCustomization, TaskDefinition } from '../common'; +import { ContributedTaskConfiguration, TaskConfiguration, TaskCustomization, TaskDefinition, ProblemMatcherContribution } from '../common'; import { TaskDefinitionRegistry } from './task-definition-registry'; +import { ProvidedTaskConfigurations } from './provided-task-configurations'; import { Disposable, DisposableCollection, ResourceProvider } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; import { FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; @@ -75,6 +76,9 @@ export class TaskConfigurations implements Disposable { @inject(TaskDefinitionRegistry) protected readonly taskDefinitionRegistry: TaskDefinitionRegistry; + @inject(ProvidedTaskConfigurations) + protected readonly providedTaskConfigurations: ProvidedTaskConfigurations; + constructor( @inject(FileSystemWatcher) protected readonly watcherServer: FileSystemWatcher, @inject(FileSystem) protected readonly fileSystem: FileSystem @@ -163,14 +167,28 @@ export class TaskConfigurations implements Disposable { return Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.keys())), [] as string[]); } - /** returns the list of known tasks */ - getTasks(): TaskConfiguration[] { - return Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.values())), [] as TaskConfiguration[]); + /** + * returns the list of known tasks, which includes: + * - all the configured tasks in `tasks.json`, and + * - the customized detected tasks + */ + async getTasks(): Promise { + 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 cus of customizations) { + const detected = await this.providedTaskConfigurations.getTaskToCustomize(cus, rootFolder); + if (detected) { + detectedTasksAsConfigured.push(detected); + } + } + } + return [...configuredTasks, ...detectedTasksAsConfigured]; } /** returns the task configuration for a given label or undefined if none */ - getTask(source: string, taskLabel: string): TaskConfiguration | undefined { - const labelConfigMap = this.tasksMap.get(source); + getTask(rootFolderPath: string, taskLabel: string): TaskConfiguration | undefined { + const labelConfigMap = this.tasksMap.get(rootFolderPath); if (labelConfigMap) { return labelConfigMap.get(taskLabel); } @@ -204,6 +222,38 @@ export class TaskConfigurations implements Disposable { return []; } + getProblemMatchers(taskConfiguration: TaskConfiguration): (string | ProblemMatcherContribution)[] { + if (!this.isDetectedTask(taskConfiguration)) { // problem matchers can be found from the task config, if it is not a detected task + if (taskConfiguration.problemMatcher) { + if (Array.isArray(taskConfiguration.problemMatcher)) { + return taskConfiguration.problemMatcher; + } + return [taskConfiguration.problemMatcher]; + } + return []; + } + + const customizationByType = this.getTaskCustomizations(taskConfiguration.taskType || taskConfiguration.type, taskConfiguration._scope) || []; + const hasCustomization = customizationByType.length > 0; + const problemMatchers: (string | ProblemMatcherContribution)[] = []; + if (hasCustomization) { + const taskDefinition = this.taskDefinitionRegistry.getDefinition(taskConfiguration); + if (taskDefinition) { + const cus = customizationByType.filter(customization => + taskDefinition.properties.required.every(rp => customization[rp] === taskConfiguration[rp]) + )[0]; // Only support having one customization per task + if (cus && cus.problemMatcher) { + if (Array.isArray(cus.problemMatcher)) { + problemMatchers.push(...cus.problemMatcher); + } else { + problemMatchers.push(cus.problemMatcher); + } + } + } + } + return problemMatchers; + } + /** returns the string uri of where the config file would be, if it existed under a given root directory */ protected getConfigFileUri(rootDir: string): string { return new URI(rootDir).resolve(this.TASKFILEPATH).resolve(this.TASKFILE).toString(); @@ -309,7 +359,8 @@ export class TaskConfigurations implements Disposable { } const configFileUri = this.getConfigFileUri(sourceFolderUri); - if (!this.getTasks().some(t => t.label === task.label)) { + const configuredAndCustomizedTasks = await this.getTasks(); + if (!configuredAndCustomizedTasks.some(t => TaskConfiguration.equals(t, task))) { await this.saveTask(configFileUri, task); } diff --git a/packages/task/src/browser/task-definition-registry.ts b/packages/task/src/browser/task-definition-registry.ts index 771e916d4f340..303aad3d488b0 100644 --- a/packages/task/src/browser/task-definition-registry.ts +++ b/packages/task/src/browser/task-definition-registry.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { injectable } from 'inversify'; -import { TaskDefinition, TaskConfiguration } from '../common'; +import { TaskConfiguration, TaskCustomization, TaskDefinition } from '../common'; @injectable() export class TaskDefinitionRegistry { @@ -41,7 +41,7 @@ export class TaskDefinitionRegistry { * @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): TaskDefinition | undefined { + getDefinition(taskConfiguration: TaskConfiguration | TaskCustomization): TaskDefinition | undefined { const definitions = this.getDefinitions(taskConfiguration.taskType || taskConfiguration.type); let matchedDefinition: TaskDefinition | undefined; let highest = -1; diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index 00591c062fe6b..bc390296892af 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -34,7 +34,6 @@ import { TaskExitedEvent, TaskInfo, TaskConfiguration, - TaskCustomization, TaskOutputProcessedEvent, RunTaskOption } from '../common'; @@ -203,13 +202,13 @@ export class TaskService implements TaskConfigurationClient { /** Returns an array of the task configurations configured in tasks.json and provided by the extensions. */ async getTasks(): Promise { - const configuredTasks = this.getConfiguredTasks(); + const configuredTasks = await this.getConfiguredTasks(); const providedTasks = await this.getProvidedTasks(); return [...configuredTasks, ...providedTasks]; } /** Returns an array of the task configurations which are configured in tasks.json files */ - getConfiguredTasks(): TaskConfiguration[] { + getConfiguredTasks(): Promise { return this.taskConfigurations.getTasks(); } @@ -304,7 +303,7 @@ export class TaskService implements TaskConfigurationClient { async run(source: string, taskLabel: string): Promise { let task = await this.getProvidedTask(source, taskLabel); const matchers: (string | ProblemMatcherContribution)[] = []; - if (!task) { // if a provided task cannot be found, search from tasks.json + if (!task) { // if a detected task cannot be found, search from tasks.json task = this.taskConfigurations.getTask(source, taskLabel); if (!task) { this.logger.error(`Can't get task launch configuration for label: ${taskLabel}`); @@ -317,9 +316,7 @@ export class TaskService implements TaskConfigurationClient { } } } else { // if a provided task is found, check if it is customized in tasks.json - const taskType = task.taskType || task.type; - const customizations = task._scope ? this.taskConfigurations.getTaskCustomizations(taskType, task._scope) : []; - const matcherContributions = this.getProblemMatchers(task, customizations); + const matcherContributions = this.taskConfigurations.getProblemMatchers(task); matchers.push(...matcherContributions); } await this.problemMatcherRegistry.onReady(); @@ -383,27 +380,6 @@ export class TaskService implements TaskConfigurationClient { } } - private getProblemMatchers(taskConfiguration: TaskConfiguration, customizations: TaskCustomization[]): (string | ProblemMatcherContribution)[] { - const hasCustomization = customizations.length > 0; - const problemMatchers: (string | ProblemMatcherContribution)[] = []; - if (hasCustomization) { - const taskDefinition = this.taskDefinitionRegistry.getDefinition(taskConfiguration); - if (taskDefinition) { - const cus = customizations.filter(customization => - taskDefinition.properties.required.every(rp => customization[rp] === taskConfiguration[rp]) - )[0]; // Only support having one customization per task - if (cus && cus.problemMatcher) { - if (Array.isArray(cus.problemMatcher)) { - problemMatchers.push(...cus.problemMatcher); - } else { - problemMatchers.push(cus.problemMatcher); - } - } - } - } - return problemMatchers; - } - private async removeProblemMarks(option?: RunTaskOption): Promise { if (option && option.customization) { const matchersFromOption = option.customization.problemMatcher || []; diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index 1cb8aa322dfb0..294bee2332fdd 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -35,7 +35,8 @@ export interface TaskConfiguration extends TaskCustomization { } export namespace TaskConfiguration { export function equals(one: TaskConfiguration, other: TaskConfiguration): boolean { - return one.label === other.label && one._source === other._source; + return (one.taskType || one.type) === (other.taskType || other.type) && + one.label === other.label && one._source === other._source; } } @@ -52,6 +53,11 @@ export interface ContributedTaskConfiguration extends TaskConfiguration { */ readonly _scope: string | undefined; } +export namespace ContributedTaskConfiguration { + export function equals(one: TaskConfiguration, other: TaskConfiguration): boolean { + return TaskConfiguration.equals(one, other) && one._scope === other._scope; + } +} /** Runtime information about Task. */ export interface TaskInfo {