From fad9056f8a55601521330dc05b41ba79a1fbbfe7 Mon Sep 17 00:00:00 2001 From: Liang Huang <liang.huang@ericsson.com> Date: Fri, 1 Nov 2019 23:29:34 -0400 Subject: [PATCH] support creating tasks.json from template - 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 <liang.huang@ericsson.com> --- packages/task/src/browser/quick-open-task.ts | 119 +++++++++++-- .../src/browser/task-configuration-manager.ts | 81 +++++---- .../task/src/browser/task-frontend-module.ts | 2 + packages/task/src/browser/task-templates.ts | 168 ++++++++++++++++++ 4 files changed, 318 insertions(+), 52 deletions(-) create mode 100644 packages/task/src/browser/task-templates.ts diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index 58eadbd558438..b5293a49b063d 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -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 { @@ -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; @@ -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, @@ -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 { @@ -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; } @@ -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 ''; diff --git a/packages/task/src/browser/task-configuration-manager.ts b/packages/task/src/browser/task-configuration-manager.ts index 8d87bbf304811..d188421617d12 100644 --- a/packages/task/src/browser/task-configuration-manager.ts +++ b/packages/task/src/browser/task-configuration-manager.ts @@ -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'; @@ -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; @@ -64,6 +68,9 @@ export class TaskConfigurationManager { this.updateModels(); } }); + this.workspaceService.onWorkspaceChanged(() => { + this.updateModels(); + }); } protected readonly models = new Map<string, TaskConfigurationModel>(); @@ -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 { diff --git a/packages/task/src/browser/task-frontend-module.ts b/packages/task/src/browser/task-frontend-module.ts index e015e3090408f..96f71519e1285 100644 --- a/packages/task/src/browser/task-frontend-module.ts +++ b/packages/task/src/browser/task-frontend-module.ts @@ -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(); @@ -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); diff --git a/packages/task/src/browser/task-templates.ts b/packages/task/src/browser/task-templates.ts new file mode 100644 index 0000000000000..06b3db420a3f3 --- /dev/null +++ b/packages/task/src/browser/task-templates.ts @@ -0,0 +1,168 @@ +/******************************************************************************** + * Copyright (C) 2019 Ericsson and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { injectable } from 'inversify'; +import { QuickPickItem } from '@theia/core/lib/common/quick-pick-service'; + +/** The representation of a task template used in the auto-generation of `tasks.json` */ +export interface TaskTemplateEntry { + id: string; + label: string; + description: string; + sort?: string; // string used in the sorting. If `undefined` the label is used in sorting. + autoDetect: boolean; // not supported in Theia + content: string; +} + +const dotnetBuild: TaskTemplateEntry = { + id: 'dotnetCore', + label: '.NET Core', + sort: 'NET Core', + autoDetect: false, // not supported in Theia + description: 'Executes .NET Core build command', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "build",', + '\t\t\t"command": "dotnet",', + '\t\t\t"type": "shell",', + '\t\t\t"args": [', + '\t\t\t\t"build",', + '\t\t\t\t// Ask dotnet build to generate full paths for file names.', + '\t\t\t\t"/property:GenerateFullPaths=true",', + '\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel', + '\t\t\t\t"/consoleloggerparameters:NoSummary"', + '\t\t\t],', + '\t\t\t"group": "build",', + '\t\t\t"presentation": {', + '\t\t\t\t"reveal": "silent"', + '\t\t\t},', + '\t\t\t"problemMatcher": "$msCompile"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +const msbuild: TaskTemplateEntry = { + id: 'msbuild', + label: 'MSBuild', + autoDetect: false, // not supported in Theia + description: 'Executes the build target', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "build",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "msbuild",', + '\t\t\t"args": [', + '\t\t\t\t// Ask msbuild to generate full paths for file names.', + '\t\t\t\t"/property:GenerateFullPaths=true",', + '\t\t\t\t"/t:build",', + '\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel', + '\t\t\t\t"/consoleloggerparameters:NoSummary"', + '\t\t\t],', + '\t\t\t"group": "build",', + '\t\t\t"presentation": {', + '\t\t\t\t// Reveal the output only if unrecognized errors occur.', + '\t\t\t\t"reveal": "silent"', + '\t\t\t},', + '\t\t\t// Use the standard MS compiler pattern to detect errors, warnings and infos', + '\t\t\t"problemMatcher": "$msCompile"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +const maven: TaskTemplateEntry = { + id: 'maven', + label: 'maven', + sort: 'MVN', + autoDetect: false, // not supported in Theia + description: 'Executes common maven commands', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "verify",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "mvn -B verify",', + '\t\t\t"group": "build"', + '\t\t},', + '\t\t{', + '\t\t\t"label": "test",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "mvn -B test",', + '\t\t\t"group": "test"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +const command: TaskTemplateEntry = { + id: 'externalCommand', + label: 'Others', + autoDetect: false, // not supported in Theia + description: 'Example to run an arbitrary external command', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "echo",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "echo Hello"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +@injectable() +export class TaskTemplateSelector { + selectTemplates(): QuickPickItem<TaskTemplateEntry>[] { + const templates: TaskTemplateEntry[] = [ + dotnetBuild, msbuild, maven + ].sort((a, b) => + (a.sort || a.label).localeCompare(b.sort || b.label) + ); + templates.push(command); + return templates.map(t => ({ + label: t.label, + description: t.description, + value: t + })); + } +}