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
+        }));
+    }
+}