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

support creating tasks.json from template #6391

Merged
merged 1 commit into from
Nov 6, 2019
Merged
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
119 changes: 102 additions & 17 deletions packages/task/src/browser/quick-open-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import { TaskActionProvider } from './task-action-provider';
import { QuickOpenHandler, QuickOpenService, QuickOpenOptions } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { FileSystem } from '@theia/filesystem/lib/common';
import { QuickOpenModel, QuickOpenItem, QuickOpenActionProvider, QuickOpenMode, QuickOpenGroupItem, QuickOpenGroupItemOptions } from '@theia/core/lib/common/quick-open-model';
import { PreferenceService } from '@theia/core/lib/browser';
import { TaskNameResolver } from './task-name-resolver';
import { TaskConfigurationManager } from './task-configuration-manager';

@injectable()
export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler {
Expand Down Expand Up @@ -54,6 +57,15 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler {
@inject(TaskNameResolver)
protected readonly taskNameResolver: TaskNameResolver;

@inject(FileSystem)
protected readonly fileSystem: FileSystem;

@inject(TaskConfigurationManager)
protected readonly taskConfigurationManager: TaskConfigurationManager;

@inject(PreferenceService)
protected readonly preferences: PreferenceService;

/** Initialize this quick open model with the tasks. */
async init(): Promise<void> {
const recentTasks = this.taskService.recentTasks;
Expand Down Expand Up @@ -167,27 +179,79 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler {
const configuredTasks = await this.taskService.getConfiguredTasks();
const providedTasks = await this.taskService.getProvidedTasks();

if (!configuredTasks.length && !providedTasks.length) {
// check if tasks.json exists. If not, display "Create tasks.json file from template"
// If tasks.json exists and empty, display 'Open tasks.json file'
let isFirstGroup = true;
const { filteredConfiguredTasks, filteredProvidedTasks } = this.getFilteredTasks([], configuredTasks, providedTasks);
const groupedTasks = this.getGroupedTasksByWorkspaceFolder([...filteredConfiguredTasks, ...filteredProvidedTasks]);
if (groupedTasks.has(undefined)) {
const configs = groupedTasks.get(undefined)!;
this.items.push(
...configs.map(taskConfig => {
const item = new TaskConfigureQuickOpenItem(
taskConfig,
this.taskService,
this.taskNameResolver,
this.workspaceService,
isMulti,
{ showBorder: false }
);
item['taskDefinitionRegistry'] = this.taskDefinitionRegistry;
return item;
})
);
isFirstGroup = false;
}

const rootUris = (await this.workspaceService.roots).map(rootStat => rootStat.uri);
for (const rootFolder of rootUris) {
const uri = new URI(rootFolder).withScheme('file');
const folderName = uri.displayName;
if (groupedTasks.has(uri.toString())) {
const configs = groupedTasks.get(uri.toString())!;
this.items.push(
...configs.map((taskConfig, index) => {
const item = new TaskConfigureQuickOpenItem(
taskConfig,
this.taskService,
this.taskNameResolver,
this.workspaceService,
isMulti,
{
groupLabel: index === 0 && isMulti ? folderName : '',
showBorder: !isFirstGroup && index === 0
}
);
item['taskDefinitionRegistry'] = this.taskDefinitionRegistry;
return item;
})
);
} else {
const { configUri } = this.preferences.resolve('tasks', [], uri.toString());
const existTaskConfigFile = !!configUri;
this.items.push(new QuickOpenGroupItem({
label: existTaskConfigFile ? 'Open tasks.json file' : 'Create tasks.json file from template',
run: (mode: QuickOpenMode): boolean => {
if (mode !== QuickOpenMode.OPEN) {
return false;
}
setTimeout(() => this.taskConfigurationManager.openConfiguration(uri.toString()));
return true;
},
showBorder: !isFirstGroup,
groupLabel: isMulti ? folderName : ''
}));
}
isFirstGroup = false;
}

if (this.items.length === 0) {
this.items.push(new QuickOpenItem({
label: 'No tasks found',
run: (_mode: QuickOpenMode): boolean => false
}));
}

const { filteredConfiguredTasks, filteredProvidedTasks } = this.getFilteredTasks([], configuredTasks, providedTasks);
this.items.push(
...filteredConfiguredTasks.map((task, index) => {
const item = new TaskConfigureQuickOpenItem(task, this.taskService, this.taskNameResolver, this.workspaceService, isMulti);
item['taskDefinitionRegistry'] = this.taskDefinitionRegistry;
return item;
}),
...filteredProvidedTasks.map((task, index) => {
const item = new TaskConfigureQuickOpenItem(task, this.taskService, this.taskNameResolver, this.workspaceService, isMulti);
item['taskDefinitionRegistry'] = this.taskDefinitionRegistry;
return item;
}),
);

this.quickOpenService.open(this, {
placeholder: 'Select a task to configure',
fuzzyMatchLabel: true,
Expand Down Expand Up @@ -234,6 +298,22 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler {
filteredRecentTasks, filteredConfiguredTasks, filteredProvidedTasks
};
}

private getGroupedTasksByWorkspaceFolder(tasks: TaskConfiguration[]): Map<string | undefined, TaskConfiguration[]> {
const grouped = new Map<string | undefined, TaskConfiguration[]>();
for (const task of tasks) {
const folder = task._scope;
if (grouped.has(folder)) {
grouped.get(folder)!.push(task);
} else {
grouped.set(folder, [task]);
}
}
for (const taskConfigs of grouped.values()) {
taskConfigs.sort((t1, t2) => t1.label.localeCompare(t2.label));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, for case when tasks.json contains invalid configuration without label
Terminal => Configure Tasks can not be open without any messages to inform user about the cause, only error in browser console is displayed

configure_error

You can use the following configuration to reproduce it:

   {
      "type": "shell",
      "command": "sleep 2 && echo test",
      "problemMatcher": []
    }

I think it can be fixed within #6482

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for pointing it out ! I am working on this bug as part of 6482 :)

}
return grouped;
}
}

export class TaskRunQuickOpenItem extends QuickOpenGroupItem {
Expand Down Expand Up @@ -324,9 +404,10 @@ export class TaskConfigureQuickOpenItem extends QuickOpenGroupItem {
protected readonly taskService: TaskService,
protected readonly taskNameResolver: TaskNameResolver,
protected readonly workspaceService: WorkspaceService,
protected readonly isMulti: boolean
protected readonly isMulti: boolean,
protected readonly options: QuickOpenGroupItemOptions
) {
super();
super(options);
const stat = this.workspaceService.workspace;
this.isMulti = stat ? !stat.isDirectory : false;
}
Expand All @@ -335,6 +416,10 @@ export class TaskConfigureQuickOpenItem extends QuickOpenGroupItem {
return this.taskNameResolver.resolve(this.task);
}

getGroupLabel(): string {
return this.options.groupLabel || '';
}

getDescription(): string {
if (!this.isMulti) {
return '';
Expand Down
81 changes: 46 additions & 35 deletions packages/task/src/browser/task-configuration-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
import { PreferenceService } from '@theia/core/lib/browser';
import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { TaskConfigurationModel } from './task-configuration-model';
import { TaskTemplateSelector } from './task-templates';
import { TaskCustomization, TaskConfiguration } from '../common/task-protocol';
import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution';
import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common';
import { FileSystem, FileSystemError /*, FileStat */ } from '@theia/filesystem/lib/common';
import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations';

Expand Down Expand Up @@ -53,6 +54,9 @@ export class TaskConfigurationManager {
@inject(WorkspaceVariableContribution)
protected readonly workspaceVariables: WorkspaceVariableContribution;

@inject(TaskTemplateSelector)
protected readonly taskTemplateSelector: TaskTemplateSelector;

protected readonly onDidChangeTaskConfigEmitter = new Emitter<FileChange>();
readonly onDidChangeTaskConfig: Event<FileChange> = this.onDidChangeTaskConfigEmitter.event;

Expand All @@ -64,6 +68,9 @@ export class TaskConfigurationManager {
this.updateModels();
}
});
this.workspaceService.onWorkspaceChanged(() => {
this.updateModels();
});
}

protected readonly models = new Map<string, TaskConfigurationModel>();
Expand Down Expand Up @@ -142,50 +149,54 @@ export class TaskConfigurationManager {
}
}

protected async doOpen(model: TaskConfigurationModel): Promise<EditorWidget> {
protected async doOpen(model: TaskConfigurationModel): Promise<EditorWidget | undefined> {
let uri = model.uri;
if (!uri) {
uri = await this.doCreate(model);
}
return this.editorManager.open(uri, {
mode: 'activate'
});
if (uri) {
return this.editorManager.open(uri, {
mode: 'activate'
});
}
}

protected async doCreate(model: TaskConfigurationModel): Promise<URI> {
await this.preferences.set('tasks', {}); // create dummy tasks.json in the correct place
const { configUri } = this.preferences.resolve('tasks'); // get uri to write content to it
let uri: URI;
if (configUri && configUri.path.base === 'tasks.json') {
uri = configUri;
} else { // fallback
uri = new URI(model.workspaceFolderUri).resolve(`${this.preferenceConfigurations.getPaths()[0]}/tasks.json`);
}
const content = this.getInitialConfigurationContent();
const fileStat = await this.filesystem.getFileStat(uri.toString());
if (!fileStat) {
throw new Error(`file not found: ${uri.toString()}`);
}
try {
await this.filesystem.setContent(fileStat, content);
} catch (e) {
if (!FileSystemError.FileExists.is(e)) {
throw e;
protected async doCreate(model: TaskConfigurationModel): Promise<URI | undefined> {
const content = await this.getInitialConfigurationContent();
if (content) {
await this.preferences.set('tasks', {}, PreferenceScope.Folder, model.workspaceFolderUri); // create dummy tasks.json in the correct place
const { configUri } = this.preferences.resolve('tasks', [], model.workspaceFolderUri); // get uri to write content to it

let uri: URI;
if (configUri && configUri.path.base === 'tasks.json') {
uri = configUri;
} else { // fallback
uri = new URI(model.workspaceFolderUri).resolve(`${this.preferenceConfigurations.getPaths()[0]}/tasks.json`);
}

const fileStat = await this.filesystem.getFileStat(uri.toString());
if (!fileStat) {
throw new Error(`file not found: ${uri.toString()}`);
}
try {
this.filesystem.setContent(fileStat, content);
} catch (e) {
if (!FileSystemError.FileExists.is(e)) {
throw e;
}
}
return uri;
}
return uri;
}

protected getInitialConfigurationContent(): string {
return `{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
"version": "2.0.0",
"tasks": ${JSON.stringify([], undefined, ' ').split('\n').map(line => ' ' + line).join('\n').trim()}
}
`;
protected async getInitialConfigurationContent(): Promise<string | undefined> {
const selected = await this.quickPick.show(this.taskTemplateSelector.selectTemplates(), {
placeholder: 'Select a Task Template'
});
if (selected) {
return selected.content;
}
}

}

export namespace TaskConfigurationManager {
Expand Down
2 changes: 2 additions & 0 deletions packages/task/src/browser/task-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { bindTaskPreferences } from './task-preferences';
import '../../src/browser/style/index.css';
import './tasks-monaco-contribution';
import { TaskNameResolver } from './task-name-resolver';
import { TaskTemplateSelector } from './task-templates';

export default new ContainerModule(bind => {
bind(TaskFrontendContribution).toSelf().inSingletonScope();
Expand Down Expand Up @@ -73,6 +74,7 @@ export default new ContainerModule(bind => {
bindContributionProvider(bind, TaskContribution);
bind(TaskSchemaUpdater).toSelf().inSingletonScope();
bind(TaskNameResolver).toSelf().inSingletonScope();
bind(TaskTemplateSelector).toSelf().inSingletonScope();

bindProcessTaskModule(bind);
bindTaskPreferences(bind);
Expand Down
Loading