Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

align "configure task" with vs code #5776

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
146 changes: 128 additions & 18 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 { TaskConfiguration, TaskCustomization, ContributedTaskConfiguration } 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 @@ -44,11 +45,14 @@ export class TaskConfigurations implements Disposable {

protected readonly toDispose = new DisposableCollection();
/**
* Map of source (path of root folder that the task config comes from) and task config map.
* 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>>();
protected taskCustomizations: TaskCustomization[] = [];
/**
* Map of source (path of root folder that the task configs come from) and task customizations map.
*/
protected taskCustomizationMap = new Map<string, TaskCustomization[]>();

protected watchedConfigFileUris: string[] = [];
protected watchersMap = new Map<string, Disposable>(); // map of watchers for task config files, where the key is folder uri
Expand All @@ -72,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 @@ -160,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 All @@ -179,8 +200,58 @@ export class TaskConfigurations implements Disposable {
this.tasksMap.delete(source);
}

getTaskCustomizations(type: string): TaskCustomization[] {
return this.taskCustomizations.filter(c => c.type === type);
removeTaskCustomizations(configFileUri: string) {
const source = this.getSourceFolderFromConfigUri(configFileUri);
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.
*/
getTaskCustomizations(type: string, rootFolder: string | undefined): TaskCustomization[] {
if (!rootFolder) {
return [];
}

const customizationInRootFolder = this.taskCustomizationMap.get(new URI(rootFolder).path.toString());
if (customizationInRootFolder) {
return customizationInRootFolder.filter(c => c.type === type);
}
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 */
Expand Down Expand Up @@ -226,19 +297,19 @@ export class TaskConfigurations implements Disposable {
// user is editing the file in the auto-save mode, having momentarily
// non-parsing JSON.
this.removeTasks(configFileUri);
this.removeTaskCustomizations(configFileUri);
const rootFolderUri = this.getSourceFolderFromConfigUri(configFileUri);

if (configuredTasksArray.length > 0) {
const newTaskMap = new Map<string, TaskConfiguration>();
for (const task of configuredTasksArray) {
newTaskMap.set(task.label, task);
}
const source = this.getSourceFolderFromConfigUri(configFileUri);
this.tasksMap.set(source, newTaskMap);
this.tasksMap.set(rootFolderUri, newTaskMap);
}

if (customizations.length > 0) {
this.taskCustomizations.length = 0;
this.taskCustomizations = customizations;
this.taskCustomizationMap.set(rootFolderUri, customizations);
}
}
}
Expand Down Expand Up @@ -275,8 +346,21 @@ export class TaskConfigurations implements Disposable {
return;
}

const configFileUri = this.getConfigFileUri(workspace.uri);
if (!this.getTasks().some(t => t.label === task.label)) {
const isDetectedTask = this.isDetectedTask(task);
let sourceFolderUri: string | undefined;
if (isDetectedTask) {
sourceFolderUri = task._scope;
} else {
sourceFolderUri = task._source;
}
if (!sourceFolderUri) {
console.error('Global task cannot be customized');
return;
}

const configFileUri = this.getConfigFileUri(sourceFolderUri);
const configuredAndCustomizedTasks = await this.getTasks();
if (!configuredAndCustomizedTasks.some(t => TaskConfiguration.equals(t, task))) {
await this.saveTask(configFileUri, task);
}

Expand All @@ -287,19 +371,38 @@ export class TaskConfigurations implements Disposable {
}
}

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.taskType || task.type };
definition.properties.all.forEach(p => {
if (task[p] !== undefined) {
customization[p] = task[p];
}
});
return {
...customization,
problemMatcher: []
};
}

/** Writes the task to a config file. Creates a config file if this one does not exist */
async saveTask(configFileUri: string, task: TaskConfiguration): Promise<void> {
if (configFileUri && !await this.fileSystem.exists(configFileUri)) {
await this.fileSystem.createFile(configFileUri);
}

const { _source, $ident, ...preparedTask } = task;
const customizedTaskTemplate = this.getTaskCustomizationTemplate(task) || preparedTask;
try {
const response = await this.fileSystem.resolveContent(configFileUri);
const content = response.content;

const formattingOptions = { tabSize: 4, insertSpaces: true, eol: '' };
const edits = jsoncparser.modify(content, ['tasks', -1], preparedTask, { formattingOptions });
const edits = jsoncparser.modify(content, ['tasks', -1], customizedTaskTemplate, { formattingOptions });
const result = jsoncparser.applyEdits(content, edits);

const resource = await this.resourceProvider(new URI(configFileUri));
Expand Down Expand Up @@ -330,8 +433,15 @@ export class TaskConfigurations implements Disposable {

/** checks if the config is a detected / contributed task */
private isDetectedTask(task: TaskConfiguration): task is ContributedTaskConfiguration {
const taskDefinition = this.taskDefinitionRegistry.getDefinition(task);
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: TaskConfiguration): TaskDefinition | undefined {
return this.taskDefinitionRegistry.getDefinition({
...task,
type: task.taskType || task.type
});
}
}
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
Loading