Skip to content

Commit

Permalink
align "task quick open" in Theia with vs code
Browse files Browse the repository at this point in the history
- The detected tasks, once customized, should be displayed as configured tasks in the quick open. (fixed #5747)
- The same task shouldn’t have more than one customization. Otherwise it would cause ambiguities and duplication in tasks.json (fixed #5719)

Signed-off-by: Liang Huang <[email protected]>
  • Loading branch information
Liang Huang committed Jul 23, 2019
1 parent be5dfb7 commit fad008a
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 () => {
Expand Down
58 changes: 53 additions & 5 deletions packages/task/src/browser/provided-task-configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<TaskConfiguration[]> {
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;
}
Expand All @@ -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<TaskConfiguration | undefined> {
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) {
Expand Down
6 changes: 3 additions & 3 deletions packages/task/src/browser/quick-open-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -66,7 +66,7 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler {
/** Initialize this quick open model with the tasks. */
async init(): Promise<void> {
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);
Expand Down Expand Up @@ -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);
}
Expand Down
65 changes: 58 additions & 7 deletions packages/task/src/browser/task-configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<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 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);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/task/src/browser/task-definition-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
********************************************************************************/

import { injectable } from 'inversify';
import { TaskDefinition, TaskConfiguration } from '../common';
import { TaskConfiguration, TaskCustomization, TaskDefinition } from '../common';

@injectable()
export class TaskDefinitionRegistry {
Expand All @@ -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;
Expand Down
32 changes: 4 additions & 28 deletions packages/task/src/browser/task-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
TaskExitedEvent,
TaskInfo,
TaskConfiguration,
TaskCustomization,
TaskOutputProcessedEvent,
RunTaskOption
} from '../common';
Expand Down Expand Up @@ -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<TaskConfiguration[]> {
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<TaskConfiguration[]> {
return this.taskConfigurations.getTasks();
}

Expand Down Expand Up @@ -304,7 +303,7 @@ export class TaskService implements TaskConfigurationClient {
async run(source: string, taskLabel: string): Promise<void> {
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}`);
Expand All @@ -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();
Expand Down Expand Up @@ -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<void> {
if (option && option.customization) {
const matchersFromOption = option.customization.problemMatcher || [];
Expand Down
8 changes: 7 additions & 1 deletion packages/task/src/common/task-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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 {
Expand Down

0 comments on commit fad008a

Please sign in to comment.