From 0dab9af83fb2635327b35cc035d1b23c8a3ecea8 Mon Sep 17 00:00:00 2001 From: Roman Nikitenko Date: Wed, 3 Apr 2019 08:39:29 +0300 Subject: [PATCH] Add ability to configure task Signed-off-by: Roman Nikitenko --- CHANGELOG.md | 1 + packages/core/src/browser/quick-open/index.ts | 1 + .../quick-open/quick-open-action-provider.ts | 100 ++++++++++++++++++ .../browser/quick-open/quick-open-model.ts | 3 +- .../src/browser/monaco-quick-open-service.ts | 81 ++++++++++++-- packages/monaco/src/typings/monaco/index.d.ts | 22 +++- .../src/plugin/tasks/task-provider.ts | 3 +- packages/task/src/browser/quick-open-task.ts | 99 +++++++++++++++-- .../src/browser/style/configure-inverse.svg | 3 + packages/task/src/browser/style/configure.svg | 3 + packages/task/src/browser/style/index.css | 25 +++++ .../task/src/browser/task-action-provider.ts | 66 ++++++++++++ .../task/src/browser/task-configurations.ts | 57 +++++++++- .../src/browser/task-frontend-contribution.ts | 18 ++++ .../task/src/browser/task-frontend-module.ts | 4 + packages/task/src/browser/task-service.ts | 14 ++- 16 files changed, 482 insertions(+), 18 deletions(-) create mode 100644 packages/core/src/browser/quick-open/quick-open-action-provider.ts create mode 100644 packages/task/src/browser/style/configure-inverse.svg create mode 100644 packages/task/src/browser/style/configure.svg create mode 100644 packages/task/src/browser/style/index.css create mode 100644 packages/task/src/browser/task-action-provider.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3eee161dcb4..d57f3a8743059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v0.6.0 - [filesystem] added the menu item `Upload Files...` to easily upload files into a workspace +- [task] added support to configure tasks - [workspace] allow creation of files and folders using recursive paths Breaking changes: diff --git a/packages/core/src/browser/quick-open/index.ts b/packages/core/src/browser/quick-open/index.ts index 8652830a95c3e..cf5bd3fa0e1b7 100644 --- a/packages/core/src/browser/quick-open/index.ts +++ b/packages/core/src/browser/quick-open/index.ts @@ -15,6 +15,7 @@ ********************************************************************************/ export * from './quick-open-model'; +export * from './quick-open-action-provider'; export * from './quick-open-service'; export * from './quick-pick-service'; export * from './quick-input-service'; diff --git a/packages/core/src/browser/quick-open/quick-open-action-provider.ts b/packages/core/src/browser/quick-open/quick-open-action-provider.ts new file mode 100644 index 0000000000000..d5deda316dccf --- /dev/null +++ b/packages/core/src/browser/quick-open/quick-open-action-provider.ts @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. 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 + ********************************************************************************/ + +import { Disposable } from '../../common/disposable'; +import { injectable } from 'inversify'; +import { QuickOpenItem } from './quick-open-model'; + +export interface QuickOpenActionProvider { + hasActions(item: QuickOpenItem): boolean; + getActions(item: QuickOpenItem): Promise; +} + +export interface QuickOpenActionOptions { + id: string; + label?: string; + tooltip?: string; + class?: string | undefined; + enabled?: boolean; + checked?: boolean; + radio?: boolean; +} + +export interface QuickOpenAction extends QuickOpenActionOptions, Disposable { + run(item?: QuickOpenItem): PromiseLike; +} + +@injectable() +export abstract class QuickOpenBaseAction implements QuickOpenAction { + constructor(protected options: QuickOpenActionOptions) { + } + + get id(): string { + return this.options.id; + } + + get label(): string { + return this.options.label || ''; + } + + set label(value: string) { + this.options.label = value; + } + + get tooltip(): string { + return this.options.tooltip || ''; + } + + set tooltip(value: string) { + this.options.tooltip = value; + } + + get class(): string | undefined { + return this.options.class || ''; + } + + set class(value: string | undefined) { + this.options.class = value; + } + + get enabled(): boolean { + return this.options.enabled || true; + } + + set enabled(value: boolean) { + this.options.enabled = value; + } + + get checked(): boolean { + return this.options.checked || false; + } + + set checked(value: boolean) { + this.options.checked = value; + } + + get radio(): boolean { + return this.options.radio || false; + } + + set radio(value: boolean) { + this.options.radio = value; + } + + abstract run(item?: QuickOpenItem): PromiseLike; + + dispose(): void { } +} diff --git a/packages/core/src/browser/quick-open/quick-open-model.ts b/packages/core/src/browser/quick-open/quick-open-model.ts index 970fb80b981b5..7b66e1046d456 100644 --- a/packages/core/src/browser/quick-open/quick-open-model.ts +++ b/packages/core/src/browser/quick-open/quick-open-model.ts @@ -16,6 +16,7 @@ import URI from '../../common/uri'; import { Keybinding } from '../keybinding'; +import { QuickOpenActionProvider } from './quick-open-action-provider'; export interface Highlight { start: number @@ -105,5 +106,5 @@ export class QuickOpenGroupItem void): void; + onType(lookFor: string, acceptor: (items: QuickOpenItem[], actionProvider?: QuickOpenActionProvider) => void): void; } diff --git a/packages/monaco/src/browser/monaco-quick-open-service.ts b/packages/monaco/src/browser/monaco-quick-open-service.ts index 6fc9d0e0ad530..934f4c79a84e9 100644 --- a/packages/monaco/src/browser/monaco-quick-open-service.ts +++ b/packages/monaco/src/browser/monaco-quick-open-service.ts @@ -17,8 +17,8 @@ import { injectable, inject, postConstruct } from 'inversify'; import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { - QuickOpenService, QuickOpenModel, QuickOpenOptions, - QuickOpenItem, QuickOpenGroupItem, QuickOpenMode, KeySequence + QuickOpenService, QuickOpenModel, QuickOpenOptions, QuickOpenItem, + QuickOpenGroupItem, QuickOpenMode, KeySequence, QuickOpenActionProvider, QuickOpenAction } from '@theia/core/lib/browser'; import { KEY_CODE_MAP } from './monaco-keycode-map'; import { ContextKey } from '@theia/core/lib/browser/context-key-service'; @@ -215,7 +215,7 @@ export class MonacoQuickOpenControllerOptsImpl implements MonacoQuickOpenControl this.options.onClose(cancelled); } - private toOpenModel(lookFor: string, items: QuickOpenItem[]): monaco.quickOpen.QuickOpenModel { + private toOpenModel(lookFor: string, items: QuickOpenItem[], actionProvider?: QuickOpenActionProvider): monaco.quickOpen.QuickOpenModel { const entries: monaco.quickOpen.QuickOpenEntry[] = []; for (const item of items) { const entry = this.createEntry(item, lookFor); @@ -226,7 +226,7 @@ export class MonacoQuickOpenControllerOptsImpl implements MonacoQuickOpenControl if (this.options.fuzzySort) { entries.sort((a, b) => monaco.quickOpen.compareEntries(a, b, lookFor)); } - return new monaco.quickOpen.QuickOpenModel(entries); + return new monaco.quickOpen.QuickOpenModel(entries, actionProvider ? new MonacoQuickOpenActionProvider(actionProvider) : undefined); } getModel(lookFor: string): monaco.quickOpen.QuickOpenModel { @@ -234,8 +234,8 @@ export class MonacoQuickOpenControllerOptsImpl implements MonacoQuickOpenControl } onType(lookFor: string, acceptor: (model: monaco.quickOpen.QuickOpenModel) => void): void { - this.model.onType(lookFor, items => { - const result = this.toOpenModel(lookFor, items); + this.model.onType(lookFor, (items, actionProvider) => { + const result = this.toOpenModel(lookFor, items, actionProvider); acceptor(result); }); } @@ -408,3 +408,72 @@ export class QuickOpenEntryGroup extends monaco.quickOpen.QuickOpenEntryGroup { } } + +export class MonacoQuickOpenAction implements monaco.quickOpen.IAction { + constructor(public readonly action: QuickOpenAction) { } + + get id(): string { + return this.action.id; + } + + get label(): string { + return this.action.label || ''; + } + + get tooltip(): string { + return this.action.tooltip || ''; + } + + get class(): string | undefined { + return this.action.class; + } + + get enabled(): boolean { + return this.action.enabled || true; + } + + get checked(): boolean { + return this.action.checked || false; + } + + get radio(): boolean { + return this.action.radio || false; + } + + // tslint:disable-next-line:no-any + run(entry: QuickOpenEntry | QuickOpenEntryGroup): PromiseLike { + return this.action.run(entry.item); + } + + dispose(): void { + this.action.dispose(); + } +} + +export class MonacoQuickOpenActionProvider implements monaco.quickOpen.IActionProvider { + constructor(public readonly provider: QuickOpenActionProvider) { } + + // tslint:disable-next-line:no-any + hasActions(element: any, entry: QuickOpenEntry | QuickOpenEntryGroup): boolean { + return this.provider.hasActions(entry.item); + } + + // tslint:disable-next-line:no-any + async getActions(element: any, entry: QuickOpenEntry | QuickOpenEntryGroup): monaco.Promise { + const actions = await this.provider.getActions(entry.item); + const monacoActions = actions.map(action => new MonacoQuickOpenAction(action)); + return monaco.Promise.wrap(monacoActions); + } + + hasSecondaryActions(): boolean { + return false; + } + + getSecondaryActions(): monaco.Promise { + return monaco.Promise.wrap([]); + } + + getActionItem() { + return undefined; + } +} diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index bb24439df9800..46985f2bf6565 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -743,8 +743,28 @@ declare module monaco.quickOpen { setShowBorder(showBorder: boolean): void; getEntry(): QuickOpenEntry | undefined; } + + export interface IAction extends IDisposable { + id: string; + label: string; + tooltip: string; + class: string | undefined; + enabled: boolean; + checked: boolean; + radio: boolean; + run(event?: any): PromiseLike; + } + + export interface IActionProvider { + hasActions(element: any, item: any): boolean; + getActions(element: any, item: any): monaco.Promise; + hasSecondaryActions(element: any, item: any): boolean; + getSecondaryActions(element: any, item: any): monaco.Promise; + getActionItem(element: any, item: any, action: IAction): any; + } + export class QuickOpenModel implements IModel, IDataSource, IFilter, IRunner { - constructor(entries?: QuickOpenEntry[] /*, actionProvider?: IActionProvider */); + constructor(entries?: QuickOpenEntry[], actionProvider?: IActionProvider); addEntries(entries: QuickOpenEntry[]): void; entries: QuickOpenEntry[]; dataSource: IDataSource; diff --git a/packages/plugin-ext/src/plugin/tasks/task-provider.ts b/packages/plugin-ext/src/plugin/tasks/task-provider.ts index 5e56fd8705fb0..3d5d81cb473e2 100644 --- a/packages/plugin-ext/src/plugin/tasks/task-provider.ts +++ b/packages/plugin-ext/src/plugin/tasks/task-provider.ts @@ -52,7 +52,8 @@ export class TaskProviderAdapter { return Promise.resolve(undefined); } const id = ObjectIdentifier.of(task); - const item = this.cache.get(id); + const cached = this.cache.get(id); + const item = cached ? cached : Converter.toTask(task); if (!item) { return Promise.resolve(undefined); } diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index 60d0ed4f8b490..ba034f9c1e21a 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -15,16 +15,22 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenGroupItem, QuickOpenMode, QuickOpenHandler, QuickOpenOptions } from '@theia/core/lib/browser/quick-open/'; +import { + QuickOpenService, QuickOpenModel, QuickOpenItem, + QuickOpenGroupItem, QuickOpenMode, QuickOpenHandler, QuickOpenOptions, QuickOpenActionProvider +} from '@theia/core/lib/browser/quick-open/'; import { TaskService } from './task-service'; import { TaskInfo, TaskConfiguration } from '../common/task-protocol'; import { TaskConfigurations } from './task-configurations'; import URI from '@theia/core/lib/common/uri'; +import { TaskActionProvider } from './task-action-provider'; +import { LabelProvider } from '@theia/core/lib/browser'; @injectable() export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { protected items: QuickOpenItem[]; + protected actionProvider: QuickOpenActionProvider | undefined; readonly prefix: string = 'task '; @@ -36,6 +42,12 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; + @inject(TaskActionProvider) + protected readonly taskActionProvider: TaskActionProvider; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + /** * @deprecated To be removed in 0.5.0 */ @@ -47,11 +59,21 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { const configuredTasks = this.taskService.getConfiguredTasks(); const providedTasks = await this.taskService.getProvidedTasks(); + const filteredProvidedTasks: TaskConfiguration[] = []; + providedTasks.forEach(provided => { + if (!configuredTasks.some(configured => configured.label === provided.label)) { + filteredProvidedTasks.push(provided); + } + }); + this.items = []; this.items.push( - ...configuredTasks.map((t, ind) => new TaskRunQuickOpenItem(t, this.taskService, true, ind === 0 ? 'configured' : undefined)), - ...providedTasks.map((t, ind) => new TaskRunQuickOpenItem(t, this.taskService, false, ind === 0 ? 'provided' : undefined)) + ...configuredTasks.map((t, ind) => new TaskRunQuickOpenItem(t, this.taskService, true, ind === 0 ? 'configured tasks' : undefined)), + ...filteredProvidedTasks.map((t, ind) => new TaskRunQuickOpenItem(t, this.taskService, false, ind === 0 ? 'detected tasks' : undefined)) ); + + this.actionProvider = this.items.length ? this.taskActionProvider : undefined; + if (!this.items.length) { this.items.push(new QuickOpenItem({ label: 'No tasks found', @@ -63,7 +85,7 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { async open(): Promise { await this.init(); this.quickOpenService.open(this, { - placeholder: 'Type the name of a task you want to execute', + placeholder: 'Select the task to run', fuzzyMatchLabel: true, fuzzySort: false }); @@ -82,6 +104,7 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { attach(): void { this.items = []; + this.actionProvider = undefined; this.taskService.getRunningTasks().then(tasks => { if (!tasks.length) { @@ -110,8 +133,31 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { }); } - onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { - acceptor(this.items); + async configure(): Promise { + this.items = []; + this.actionProvider = undefined; + + const providedTasks = await this.taskService.getProvidedTasks(); + if (!providedTasks.length) { + this.items.push(new QuickOpenItem({ + label: 'No tasks found', + run: (_mode: QuickOpenMode): boolean => false + })); + } + + providedTasks.forEach(task => { + this.items.push(new TaskConfigureQuickOpenItem(task, this.taskService, this.labelProvider)); + }); + + this.quickOpenService.open(this, { + placeholder: 'Select a task to configure', + fuzzyMatchLabel: true, + fuzzySort: true + }); + } + + onType(lookFor: string, acceptor: (items: QuickOpenItem[], actionProvider?: QuickOpenActionProvider) => void): void { + acceptor(this.items, this.actionProvider); } protected getRunningTaskLabel(task: TaskInfo): string { @@ -130,6 +176,10 @@ export class TaskRunQuickOpenItem extends QuickOpenGroupItem { super(); } + getTask(): TaskConfiguration { + return this.task; + } + getLabel(): string { if (this.isConfigured) { return `${this.task.type}: ${this.task.label}`; @@ -155,7 +205,12 @@ export class TaskRunQuickOpenItem extends QuickOpenGroupItem { if (mode !== QuickOpenMode.OPEN) { return false; } - this.taskService.run(this.task._source, this.task.label); + + if (this.isConfigured) { + this.taskService.runConfiguredTask(this.task._source, this.task.label); + } else { + this.taskService.run(this.task._source, this.task.label); + } return true; } @@ -185,3 +240,33 @@ export class TaskAttachQuickOpenItem extends QuickOpenItem { return true; } } +export class TaskConfigureQuickOpenItem extends QuickOpenGroupItem { + + constructor( + protected readonly task: TaskConfiguration, + protected readonly taskService: TaskService, + protected readonly labelProvider: LabelProvider + ) { + super(); + } + + getLabel(): string { + return `${this.task._source}: ${this.task.label}`; + } + + getDescription(): string { + if (this.task._scope) { + return this.labelProvider.getLongName(new URI(this.task._scope)); + } + return this.task._source; + } + + run(mode: QuickOpenMode): boolean { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + this.taskService.configure(this.task); + + return true; + } +} diff --git a/packages/task/src/browser/style/configure-inverse.svg b/packages/task/src/browser/style/configure-inverse.svg new file mode 100644 index 0000000000000..692b34b2c1647 --- /dev/null +++ b/packages/task/src/browser/style/configure-inverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/task/src/browser/style/configure.svg b/packages/task/src/browser/style/configure.svg new file mode 100644 index 0000000000000..cdd6588df5c12 --- /dev/null +++ b/packages/task/src/browser/style/configure.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/task/src/browser/style/index.css b/packages/task/src/browser/style/index.css new file mode 100644 index 0000000000000..84168e26ab32e --- /dev/null +++ b/packages/task/src/browser/style/index.css @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. 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 + ********************************************************************************/ + + .quick-open-task-configure-dark { + background-image: url('configure-inverse.svg'); + background-position-y: -2px; +} + +.quick-open-task-configure-bright { + background-image: url('configure.svg'); + background-position-y: -2px; +} diff --git a/packages/task/src/browser/task-action-provider.ts b/packages/task/src/browser/task-action-provider.ts new file mode 100644 index 0000000000000..736330452f3fe --- /dev/null +++ b/packages/task/src/browser/task-action-provider.ts @@ -0,0 +1,66 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. 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 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { TaskService } from './task-service'; +import { TaskRunQuickOpenItem } from './quick-open-task'; +import { QuickOpenBaseAction, QuickOpenItem, QuickOpenActionProvider, QuickOpenAction } from '@theia/core/lib/browser/quick-open'; +import { ThemeService } from '@theia/core/lib/browser/theming'; + +@injectable() +export class ConfigureTaskAction extends QuickOpenBaseAction { + + @inject(TaskService) + protected readonly taskService: TaskService; + + constructor() { + super({ id: 'configure:task' }); + + this.updateTheme(); + + ThemeService.get().onThemeChange(() => this.updateTheme()); + } + + async run(item?: QuickOpenItem): Promise { + if (item instanceof TaskRunQuickOpenItem) { + this.taskService.configure(item.getTask()); + } + } + + protected updateTheme(): void { + const theme = ThemeService.get().getCurrentTheme().id; + if (theme === 'dark') { + this.class = 'quick-open-task-configure-dark'; + } else if (theme === 'light') { + this.class = 'quick-open-task-configure-bright'; + } + } +} + +@injectable() +export class TaskActionProvider implements QuickOpenActionProvider { + + @inject(ConfigureTaskAction) + protected configureTaskAction: ConfigureTaskAction; + + hasActions(): boolean { + return true; + } + + async getActions(): Promise { + return [this.configureTaskAction]; + } +} diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index 825f6eebd7c80..c84bbbd0c5dae 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -16,13 +16,16 @@ import { inject, injectable } from 'inversify'; import { TaskConfiguration } from '../common/task-protocol'; -import { Disposable, DisposableCollection } from '@theia/core/lib/common'; +import { Disposable, DisposableCollection, ResourceProvider } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; import { FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; import { FileSystem } from '@theia/filesystem/lib/common'; import * as jsoncparser from 'jsonc-parser'; import { ParseError } from 'jsonc-parser'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { open, OpenerService } from '@theia/core/lib/browser'; +import { Resource } from '@theia/core'; export interface TaskConfigurationClient { /** @@ -54,6 +57,15 @@ export class TaskConfigurations implements Disposable { protected client: TaskConfigurationClient | undefined = undefined; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + + @inject(ResourceProvider) + protected readonly resourceProvider: ResourceProvider; + + @inject(OpenerService) + protected readonly openerService: OpenerService; + constructor( @inject(FileSystemWatcher) protected readonly watcherServer: FileSystemWatcher, @inject(FileSystem) protected readonly fileSystem: FileSystem @@ -230,6 +242,49 @@ export class TaskConfigurations implements Disposable { } } + /** Adds given task to a config file and opens the file to provide ability to edit task configuration. */ + async configure(task: TaskConfiguration): Promise { + const workspace = this.workspaceService.workspace; + if (!workspace) { + return; + } + + const configFileUri = this.getConfigFileUri(workspace.uri); + if (!this.getTasks().some(t => t.label === task.label)) { + await this.saveTask(configFileUri, task); + } + + try { + await open(this.openerService, new URI(configFileUri)); + } catch (e) { + console.error(`Error occurred while opening: ${this.TASKFILE}.`, e); + } + } + + /** Writes the task to a config file. Creates a config file if this one does not exist */ + async saveTask(configFileUri: string, task: TaskConfiguration): Promise { + if (configFileUri && !await this.fileSystem.exists(configFileUri)) { + await this.fileSystem.createFile(configFileUri); + } + + const { _source, $ident, ...preparedTask } = task; + try { + const response = await this.fileSystem.resolveContent(configFileUri); + const content = response.content; + + const formattingOptions = { tabSize: 4, insertSpaces: true, eol: '' }; + const edits = jsoncparser.modify(content, ['tasks', -1], preparedTask, { formattingOptions }); + const result = jsoncparser.applyEdits(content, edits); + + const resource = await this.resourceProvider(new URI(configFileUri)); + Resource.save(resource, { content: result }); + } catch (e) { + const message = `Failed to save task configuration for ${task.label} task.`; + console.error(`${message} ${e.toString()}`); + return; + } + } + protected filterDuplicates(tasks: TaskConfiguration[]): TaskConfiguration[] { const filteredTasks: TaskConfiguration[] = []; for (const task of tasks) { diff --git a/packages/task/src/browser/task-frontend-contribution.ts b/packages/task/src/browser/task-frontend-contribution.ts index 70e1f034b5f1b..2cbd3adb70f36 100644 --- a/packages/task/src/browser/task-frontend-contribution.ts +++ b/packages/task/src/browser/task-frontend-contribution.ts @@ -53,6 +53,12 @@ export namespace TaskCommands { category: TASK_CATEGORY, label: 'Run Selected Text' }; + + export const TASK_CONFIGURE: Command = { + id: 'task:configure', + category: TASK_CATEGORY, + label: 'Configure Tasks...' + }; } @injectable() @@ -132,6 +138,13 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri execute: () => this.taskService.runSelectedText() } ); + + registry.registerCommand( + TaskCommands.TASK_CONFIGURE, + { + execute: () => this.quickOpenTask.configure() + } + ); } registerMenus(menus: MenuModelRegistry): void { @@ -154,6 +167,11 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri commandId: TaskCommands.TASK_RUN_TEXT.id, order: '3' }); + + menus.registerMenuAction(TerminalMenus.TERMINAL_TASKS, { + commandId: TaskCommands.TASK_CONFIGURE.id, + order: '4' + }); } registerQuickOpenHandlers(handlers: QuickOpenHandlerRegistry): void { diff --git a/packages/task/src/browser/task-frontend-module.ts b/packages/task/src/browser/task-frontend-module.ts index 00d84180ada1d..8e95eba0efddc 100644 --- a/packages/task/src/browser/task-frontend-module.ts +++ b/packages/task/src/browser/task-frontend-module.ts @@ -29,10 +29,14 @@ import { TaskServer, taskPath } from '../common/task-protocol'; import { TaskWatcher } from '../common/task-watcher'; import { bindProcessTaskModule } from './process/process-task-frontend-module'; import { TaskSchemaUpdater } from './task-schema-updater'; +import { TaskActionProvider, ConfigureTaskAction } from './task-action-provider'; +import '../../src/browser/style/index.css'; export default new ContainerModule(bind => { bind(TaskFrontendContribution).toSelf().inSingletonScope(); bind(TaskService).toSelf().inSingletonScope(); + bind(TaskActionProvider).toSelf().inSingletonScope(); + bind(ConfigureTaskAction).toSelf().inSingletonScope(); for (const identifier of [FrontendApplicationContribution, CommandContribution, KeybindingContribution, MenuContribution, QuickOpenContribution]) { bind(identifier).toService(TaskFrontendContribution); diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index 9aabbea47c497..a0e7d2108e544 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -190,7 +190,8 @@ export class TaskService implements TaskConfigurationClient { this.logger.error(`Can't get task launch configuration for label: ${taskLabel}`); return; } - this.run(task._source, task.label); + + this.runTask(task); } /** @@ -218,6 +219,13 @@ export class TaskService implements TaskConfigurationClient { } } + this.runTask(task); + } + + async runTask(task: TaskConfiguration): Promise { + const source = task._source; + const taskLabel = task.label; + const resolver = this.taskResolverRegistry.getResolver(task.type); let resolvedTask: TaskConfiguration; try { @@ -288,6 +296,10 @@ export class TaskService implements TaskConfigurationClient { widget.start(terminalId); } + async configure(task: TaskConfiguration): Promise { + await this.taskConfigurations.configure(task); + } + protected isEventForThisClient(context: string | undefined): boolean { if (context === this.getContext()) { return true;