Skip to content

Commit

Permalink
support creating tasks.json from template
Browse files Browse the repository at this point in the history
- group the tasks by workspace folder in the quick open item list populated by "Terminal" -> "Configure Tasks..."
- when "Configure Tasks" is started, check if tasks.json exists:
  - "Create tasks.json file from template" is displayed if 1) there are no detected or configured tasks, and 2) tasks.json does not exist
  - "Open tasks.json file" is displayed, if 1) tasks.json exists, and 2) no detected or configured tasks are found.
- add VS Code Task templates to Theia. CQ created http://dev.eclipse.org/ipzilla/show_bug.cgi?id=20967
- part of #4212

Signed-off-by: Liang Huang <[email protected]>
  • Loading branch information
Liang Huang committed Nov 2, 2019
1 parent 05f2e07 commit fad9056
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 52 deletions.
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));
}
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

0 comments on commit fad9056

Please sign in to comment.