Skip to content

Commit

Permalink
prompt user to choose parser to parse task output
Browse files Browse the repository at this point in the history
- before a task runs, prompt the user to choose which parser to use to
parse the task output, and write user's choice into `tasks.json`
- part of #4212

Signed-off-by: Liang Huang <liang.huang@ericsson.com>
  • Loading branch information
Liang Huang committed Aug 7, 2019
1 parent b40a8d5 commit 5e35d9a
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [task] fixed the problem where a detected task can be customized more than once [#5777](https://github.com/theia-ide/theia/pull/5777)
- [task] displayed the customized tasks as "configured tasks" in the task quick open [#5777](https://github.com/theia-ide/theia/pull/5777)
- [task] allowed users to override any task properties other than the ones used in the task definition [#5777](https://github.com/theia-ide/theia/pull/5777)
- [task] prompt user to choose parser to parse task output [#5877](https://github.com/theia-ide/theia/pull/5877)

Breaking changes:

Expand Down
82 changes: 74 additions & 8 deletions packages/task/src/browser/task-configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,13 +343,7 @@ export class TaskConfigurations implements Disposable {
return;
}

const isDetectedTask = this.isDetectedTask(task);
let sourceFolderUri: string | undefined;
if (isDetectedTask) {
sourceFolderUri = task._scope;
} else {
sourceFolderUri = task._source;
}
const sourceFolderUri: string | undefined = this.getSourceFolderUriFromTask(task);
if (!sourceFolderUri) {
console.error('Global task cannot be customized');
return;
Expand Down Expand Up @@ -380,9 +374,25 @@ export class TaskConfigurations implements Disposable {
customization[p] = task[p];
}
});
const problemMatcher: string[] = [];
if (task.problemMatcher) {
if (Array.isArray(task.problemMatcher)) {
problemMatcher.push(...task.problemMatcher.map(t => {
if (typeof t === 'string') {
return t;
} else {
return t.name!;
}
}));
} else if (typeof task.problemMatcher === 'string') {
problemMatcher.push(task.problemMatcher);
} else {
problemMatcher.push(task.problemMatcher.name!);
}
}
return {
...customization,
problemMatcher: []
problemMatcher: problemMatcher.map(name => name.startsWith('$') ? name : `$${name}`)
};
}

Expand Down Expand Up @@ -411,6 +421,51 @@ 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)
*/
async saveProblemMatcherForTask(task: TaskConfiguration, problemMatchers: string[]): Promise<void> {
const sourceFolderUri: string | undefined = this.getSourceFolderUriFromTask(task);
if (!sourceFolderUri) {
console.error('Global task cannot be customized');
return;
}
const configFileUri = this.getConfigFileUri(sourceFolderUri);
const configuredAndCustomizedTasks = await this.getTasks();
if (configuredAndCustomizedTasks.some(t => ContributedTaskConfiguration.equals(t, task))) { // task is already in `tasks.json`
try {
const content = (await this.fileSystem.resolveContent(configFileUri)).content;
const errors: ParseError[] = [];
const jsonTasks = jsoncparser.parse(content, errors).tasks;
if (errors.length > 0) {
for (const e of errors) {
console.error(`Error parsing ${configFileUri}: error: ${e.error}, length: ${e.length}, offset: ${e.offset}`);
}
} else {
if (jsonTasks) {
const ind = jsonTasks.findIndex((t: TaskConfiguration) => t.type === task.type && t.label === task.label);
const newTask = Object.assign(jsonTasks[ind], { problemMatcher: problemMatchers.map(name => name.startsWith('$') ? name : `$${name}`) });
jsonTasks[ind] = newTask;
}
const updatedTasks = JSON.stringify({ tasks: jsonTasks });
const formattingOptions = { tabSize: 4, insertSpaces: true, eol: '' };
const edits = jsoncparser.format(updatedTasks, undefined, formattingOptions);
const updatedContent = jsoncparser.applyEdits(updatedTasks, edits);
const resource = await this.resourceProvider(new URI(configFileUri));
Resource.save(resource, { content: updatedContent });
}
} catch (e) {
console.error(`Failed to save task configuration for ${task.label} task. ${e.toString()}`);
return;
}
} else { // task is not in `tasks.json`
task.problemMatcher = problemMatchers;
this.saveTask(configFileUri, task);
}
}

protected filterDuplicates(tasks: TaskConfiguration[]): TaskConfiguration[] {
const filteredTasks: TaskConfiguration[] = [];
for (const task of tasks) {
Expand Down Expand Up @@ -441,4 +496,15 @@ export class TaskConfigurations implements Disposable {
type: task.taskType || task.type
});
}

private getSourceFolderUriFromTask(task: TaskConfiguration): string | undefined {
const isDetectedTask = this.isDetectedTask(task);
let sourceFolderUri: string | undefined;
if (isDetectedTask) {
sourceFolderUri = task._scope;
} else {
sourceFolderUri = task._source;
}
return sourceFolderUri;
}
}
11 changes: 11 additions & 0 deletions packages/task/src/browser/task-problem-matcher-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ export class ProblemMatcherRegistry {
return this.matchers[name];
}

/**
* Returns all registered problem matchers in the registry.
*/
getAll(): NamedProblemMatcher[] {
const all: NamedProblemMatcher[] = [];
for (const matcherName of Object.keys(this.matchers)) {
all.push(this.get(matcherName)!);
}
return all;
}

/**
* Transforms the `ProblemMatcherContribution` to a `ProblemMatcher`
*
Expand Down
189 changes: 137 additions & 52 deletions packages/task/src/browser/task-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,27 @@ import { inject, injectable, named, postConstruct } from 'inversify';
import { EditorManager } from '@theia/editor/lib/browser';
import { ILogger } from '@theia/core/lib/common';
import { ApplicationShell, FrontendApplication, WidgetManager } from '@theia/core/lib/browser';
import { QuickPickService, QuickPickItem } from '@theia/core/lib/common/quick-pick-service';
import { TaskResolverRegistry, TaskProviderRegistry } from './task-contribution';
import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions } from '@theia/terminal/lib/browser/terminal-widget-impl';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { VariableResolverService } from '@theia/variable-resolver/lib/browser';
import {
ContributedTaskConfiguration,
NamedProblemMatcher,
ProblemMatcher,
ProblemMatchData,
TaskConfiguration,
TaskCustomization,
TaskServer,
TaskExitedEvent,
TaskInfo,
TaskConfiguration,
TaskOutputProcessedEvent,
TaskServer,
RunTaskOption
} from '../common';
import { TaskWatcher } from '../common/task-watcher';
Expand All @@ -46,6 +49,11 @@ import { ProblemMatcherRegistry } from './task-problem-matcher-registry';
import { Range } from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';

export interface QuickPickProblemMatcherItem {
problemMatchers: NamedProblemMatcher[] | undefined;
learnMore?: boolean;
}

@injectable()
export class TaskService implements TaskConfigurationClient {
/**
Expand Down Expand Up @@ -111,6 +119,12 @@ export class TaskService implements TaskConfigurationClient {
@inject(ProblemMatcherRegistry)
protected readonly problemMatcherRegistry: ProblemMatcherRegistry;

@inject(QuickPickService)
protected readonly quickPick: QuickPickService;

@inject(OpenerService)
protected readonly openerService: OpenerService;

/**
* @deprecated To be removed in 0.5.0
*/
Expand Down Expand Up @@ -312,7 +326,7 @@ export class TaskService implements TaskConfigurationClient {
if (!task) {
this.logger.error(`Can't get task launch configuration for label: ${taskLabel}`);
return;
} else if (task.problemMatcher) {
} else {
Object.assign(customizationObject, {
type: task.type,
problemMatcher: task.problemMatcher
Expand All @@ -324,36 +338,67 @@ export class TaskService implements TaskConfigurationClient {
Object.assign(customizationObject, customizationFound);
}
}
await this.problemMatcherRegistry.onReady();
const notResolvedMatchers = customizationObject.problemMatcher ?
(Array.isArray(customizationObject.problemMatcher) ? customizationObject.problemMatcher : [customizationObject.problemMatcher]) : [];
const resolvedMatchers: ProblemMatcher[] = [];
// resolve matchers before passing them to the server
for (const matcher of notResolvedMatchers) {
let resolvedMatcher: ProblemMatcher | undefined;
if (typeof matcher === 'string') {
resolvedMatcher = this.problemMatcherRegistry.get(matcher);
} else {
resolvedMatcher = await this.problemMatcherRegistry.getProblemMatcherFromContribution(matcher);

if (!customizationObject.problemMatcher) {
// ask the user what s/he wants to use to parse the task output
const items = this.getCustomizeProblemMatcherItems();
const selected = await this.quickPick.show(items, {
placeholder: 'Select for which kind of errors and warnings to scan the task output'
});
if (selected) {
if (selected.problemMatchers) {
let matcherNames: string[] = [];
if (selected.problemMatchers && selected.problemMatchers.length === 0) { // never parse output for this task
matcherNames = [];
} else if (selected.problemMatchers && selected.problemMatchers.length > 0) { // continue with user-selected parser
matcherNames = selected.problemMatchers.map(matcher => matcher.name);
}
customizationObject.problemMatcher = matcherNames;

// write the selected matcher (or the decision of "never parse") into the `tasks.json`
this.taskConfigurations.saveProblemMatcherForTask(task, 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'));
}
// else, continue the task with no parser
} else { // do not start the task in case that the user did not select any item from the list
return;
}
if (resolvedMatcher) {
const scope = task._scope || task._source;
if (resolvedMatcher.filePrefix && scope) {
const options = { context: new URI(scope).withScheme('file') };
const resolvedPrefix = await this.variableResolverService.resolve(resolvedMatcher.filePrefix, options);
Object.assign(resolvedMatcher, { filePrefix: resolvedPrefix });
}

const notResolvedMatchers = customizationObject.problemMatcher ?
(Array.isArray(customizationObject.problemMatcher) ? customizationObject.problemMatcher : [customizationObject.problemMatcher]) : undefined;
let resolvedMatchers: ProblemMatcher[] | undefined = [];
if (notResolvedMatchers) {
// resolve matchers before passing them to the server
for (const matcher of notResolvedMatchers) {
let resolvedMatcher: ProblemMatcher | undefined;
await this.problemMatcherRegistry.onReady();
if (typeof matcher === 'string') {
resolvedMatcher = this.problemMatcherRegistry.get(matcher);
} else {
resolvedMatcher = await this.problemMatcherRegistry.getProblemMatcherFromContribution(matcher);
}
if (resolvedMatcher) {
const scope = task._scope || task._source;
if (resolvedMatcher.filePrefix && scope) {
const options = { context: new URI(scope).withScheme('file') };
const resolvedPrefix = await this.variableResolverService.resolve(resolvedMatcher.filePrefix, options);
Object.assign(resolvedMatcher, { filePrefix: resolvedPrefix });
}
resolvedMatchers.push(resolvedMatcher);
}
resolvedMatchers.push(resolvedMatcher);
}
} else {
resolvedMatchers = undefined;
}

this.runTask(task, {
customization: { ...customizationObject, ...{ problemMatcher: resolvedMatchers } }
});
}

async runTask(task: TaskConfiguration, option?: RunTaskOption): Promise<void> {
const source = task._source;
const taskLabel = task.label;
if (option && option.customization) {
const taskDefinition = this.taskDefinitionRegistry.getDefinition(task);
if (taskDefinition) { // use the customization object to override the task config
Expand All @@ -366,35 +411,11 @@ export class TaskService implements TaskConfigurationClient {
}
}

const resolver = this.taskResolverRegistry.getResolver(task.type);
let resolvedTask: TaskConfiguration;
try {
resolvedTask = resolver ? await resolver.resolveTask(task) : task;
this.addRecentTasks(task);
} catch (error) {
this.logger.error(`Error resolving task '${taskLabel}': ${error}`);
this.messageService.error(`Error resolving task '${taskLabel}': ${error}`);
return;
}

await this.removeProblemMarks(option);

let taskInfo: TaskInfo;
try {
taskInfo = await this.taskServer.run(resolvedTask, this.getContext(), option);
this.lastTask = { source, taskLabel };
} catch (error) {
const errorStr = `Error launching task '${taskLabel}': ${error.message}`;
this.logger.error(errorStr);
this.messageService.error(errorStr);
return;
}

this.logger.debug(`Task created. Task id: ${taskInfo.taskId}`);

// open terminal widget if the task is based on a terminal process (type: shell)
if (taskInfo.terminalId !== undefined) {
this.attach(taskInfo.terminalId, taskInfo.taskId);
const resolvedTask = await this.getResolvedTask(task);
if (resolvedTask) {
// remove problem markers from the same source before running the task
await this.removeProblemMarks(option);
this.runResolvedTask(resolvedTask, option);
}
}

Expand All @@ -412,6 +433,70 @@ export class TaskService implements TaskConfigurationClient {
}
}

private async getResolvedTask(task: TaskConfiguration): Promise<TaskConfiguration | undefined> {
const resolver = this.taskResolverRegistry.getResolver(task.type);
try {
const resolvedTask = resolver ? await resolver.resolveTask(task) : task;
this.addRecentTasks(task);
return resolvedTask;
} catch (error) {
const errMessage = `Error resolving task '${task.label}': ${error}`;
this.logger.error(errMessage);
this.messageService.error(errMessage);
}
}

/**
* Runs the resolved task and opens terminal widget if the task is based on a terminal process
* @param resolvedTask the resolved task
* @param option options to run the resolved task
*/
private async runResolvedTask(resolvedTask: TaskConfiguration, option?: RunTaskOption): Promise<void> {
const source = resolvedTask._source;
const taskLabel = resolvedTask.label;
try {
const taskInfo = await this.taskServer.run(resolvedTask, this.getContext(), option);
this.lastTask = { source, taskLabel };
this.logger.debug(`Task created. Task id: ${taskInfo.taskId}`);

// open terminal widget if the task is based on a terminal process (type: shell)
if (taskInfo && taskInfo.terminalId !== undefined) {
this.attach(taskInfo.terminalId, taskInfo.taskId);
}
} catch (error) {
const errorStr = `Error launching task '${taskLabel}': ${error.message}`;
this.logger.error(errorStr);
this.messageService.error(errorStr);
}
}

private getCustomizeProblemMatcherItems(): QuickPickItem<QuickPickProblemMatcherItem>[] {
const items: QuickPickItem<QuickPickProblemMatcherItem>[] = [];
items.push({
label: 'Continue without scanning the task output',
value: { problemMatchers: undefined }
});
items.push({
label: 'Never scan the task output',
value: { problemMatchers: [] }
});
items.push({
label: 'Learn more about scanning the task output',
value: { problemMatchers: undefined, learnMore: true }
});
items.push({ type: 'separator', label: 'registered parsers' });

const registeredProblemMatchers = this.problemMatcherRegistry.getAll();
items.push(...registeredProblemMatchers.map(matcher =>
({
label: matcher.label,
value: { problemMatchers: [matcher] },
description: matcher.name
})
));
return items;
}

/**
* Run selected text in the last active terminal.
*/
Expand Down

0 comments on commit 5e35d9a

Please sign in to comment.