From 415d602929485070d956bc553002927af760c681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Fri, 17 Apr 2020 14:58:19 +0200 Subject: [PATCH] Enable user-level task configurations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- .../preferences/preference-provider.ts | 10 +- .../launch-folder-preference-provider.ts | 42 ----- .../browser/preferences/launch-preferences.ts | 3 - .../plugin-ext/src/common/plugin-api-rpc.ts | 2 +- .../plugin-ext/src/main/browser/tasks-main.ts | 22 ++- .../src/plugin/type-converters.spec.ts | 7 +- .../plugin-ext/src/plugin/type-converters.ts | 10 +- .../src/browser/folder-preference-provider.ts | 20 +-- .../browser/folders-preferences-provider.ts | 25 +-- packages/preferences/src/browser/index.ts | 1 + .../src/browser/preference-bindings.ts | 35 ++-- .../browser/section-preference-provider.ts | 83 ++++++++++ .../user-configs-preference-provider.ts | 115 +++++++++++++ .../src/browser/user-preference-provider.ts | 16 +- .../browser/process/process-task-resolver.ts | 16 +- .../browser/provided-task-configurations.ts | 20 +-- packages/task/src/browser/quick-open-task.ts | 56 +++---- .../src/browser/task-configuration-manager.ts | 83 ++++++---- .../src/browser/task-configuration-model.ts | 27 ++- .../task/src/browser/task-configurations.ts | 154 ++++++++++-------- .../src/browser/task-definition-registry.ts | 8 +- .../task-folder-preference-provider.ts | 42 ----- .../src/browser/task-frontend-contribution.ts | 15 ++ .../task/src/browser/task-name-resolver.ts | 4 +- packages/task/src/browser/task-preferences.ts | 3 - .../task/src/browser/task-schema-updater.ts | 3 +- packages/task/src/browser/task-service.ts | 35 ++-- .../task/src/browser/task-source-resolver.ts | 17 +- packages/task/src/common/task-protocol.ts | 14 +- 29 files changed, 549 insertions(+), 339 deletions(-) delete mode 100644 packages/debug/src/browser/preferences/launch-folder-preference-provider.ts create mode 100644 packages/preferences/src/browser/section-preference-provider.ts create mode 100644 packages/preferences/src/browser/user-configs-preference-provider.ts delete mode 100644 packages/task/src/browser/task-folder-preference-provider.ts diff --git a/packages/core/src/browser/preferences/preference-provider.ts b/packages/core/src/browser/preferences/preference-provider.ts index 1c0ad63f677d0..c6366bc0511c6 100644 --- a/packages/core/src/browser/preferences/preference-provider.ts +++ b/packages/core/src/browser/preferences/preference-provider.ts @@ -32,8 +32,16 @@ export interface PreferenceProviderDataChange { readonly domain?: string[]; } +export namespace PreferenceProviderDataChange { + export function affects(change: PreferenceProviderDataChange, resourceUri?: string): boolean { + const resourcePath = resourceUri && new URI(resourceUri).path; + const domain = change.domain; + return !resourcePath || !domain || domain.some(uri => new URI(uri).path.relativity(resourcePath) >= 0); + } +} + export interface PreferenceProviderDataChanges { - [preferenceName: string]: PreferenceProviderDataChange + [preferenceName: string]: PreferenceProviderDataChange; } export interface PreferenceResolveResult { diff --git a/packages/debug/src/browser/preferences/launch-folder-preference-provider.ts b/packages/debug/src/browser/preferences/launch-folder-preference-provider.ts deleted file mode 100644 index 7168ff266c42e..0000000000000 --- a/packages/debug/src/browser/preferences/launch-folder-preference-provider.ts +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************** - * 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 } from 'inversify'; -import { FolderPreferenceProvider } from '@theia/preferences/lib/browser/folder-preference-provider'; - -@injectable() -export class LaunchFolderPreferenceProvider extends FolderPreferenceProvider { - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected parse(content: string): any { - const launch = super.parse(content); - if (launch === undefined) { - return undefined; - } - return { launch: { ...launch } }; - } - - protected getPath(preferenceName: string): string[] | undefined { - if (preferenceName === 'launch') { - return []; - } - if (preferenceName.startsWith('launch.')) { - return [preferenceName.substr('launch.'.length)]; - } - return undefined; - } - -} diff --git a/packages/debug/src/browser/preferences/launch-preferences.ts b/packages/debug/src/browser/preferences/launch-preferences.ts index a5c0d059d720f..743f8fd4fc886 100644 --- a/packages/debug/src/browser/preferences/launch-preferences.ts +++ b/packages/debug/src/browser/preferences/launch-preferences.ts @@ -17,8 +17,6 @@ import { interfaces } from 'inversify'; import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; import { launchSchemaId } from '../debug-schema-updater'; -import { LaunchFolderPreferenceProvider } from './launch-folder-preference-provider'; -import { FolderPreferenceProvider } from '@theia/preferences/lib/browser'; import { PreferenceConfiguration } from '@theia/core/lib/browser/preferences/preference-configurations'; export const launchPreferencesSchema: PreferenceSchema = { @@ -35,6 +33,5 @@ export const launchPreferencesSchema: PreferenceSchema = { export function bindLaunchPreferences(bind: interfaces.Bind): void { bind(PreferenceContribution).toConstantValue({ schema: launchPreferencesSchema }); - bind(FolderPreferenceProvider).to(LaunchFolderPreferenceProvider).inTransientScope().whenTargetNamed('launch'); bind(PreferenceConfiguration).toConstantValue({ name: 'launch' }); } diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index f0bad897aa54f..e89adacda804b 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1136,7 +1136,7 @@ export interface TaskDto { type: string; label: string; source?: string; - scope?: string; + scope: string | number; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } diff --git a/packages/plugin-ext/src/main/browser/tasks-main.ts b/packages/plugin-ext/src/main/browser/tasks-main.ts index 98a07c843e576..84c0c41d9a7ca 100644 --- a/packages/plugin-ext/src/main/browser/tasks-main.ts +++ b/packages/plugin-ext/src/main/browser/tasks-main.ts @@ -52,7 +52,7 @@ export class TasksMainImpl implements TasksMain, Disposable { this.toDispose.push(this.taskWatcher.onTaskCreated((event: TaskInfo) => { this.proxy.$onDidStartTask({ id: event.taskId, - task: event.config + task: this.fromTaskConfiguration(event.config) }); })); @@ -64,7 +64,7 @@ export class TasksMainImpl implements TasksMain, Disposable { if (event.processId !== undefined) { this.proxy.$onDidStartTaskProcess(event.processId, { id: event.taskId, - task: event.config + task: this.fromTaskConfiguration(event.config) }); } })); @@ -133,19 +133,19 @@ export class TasksMainImpl implements TasksMain, Disposable { if (taskInfo) { return { id: taskInfo.taskId, - task: taskInfo.config + task: this.fromTaskConfiguration(taskInfo.config) }; } } async $taskExecutions(): Promise<{ id: number; - task: TaskConfiguration; + task: TaskDto; }[]> { const runningTasks = await this.taskService.getRunningTasks(); return runningTasks.map(taskInfo => ({ id: taskInfo.taskId, - task: taskInfo.config + task: this.fromTaskConfiguration(taskInfo.config) })); } @@ -167,7 +167,7 @@ export class TasksMainImpl implements TasksMain, Disposable { protected createTaskResolver(handle: number): TaskResolver { return { resolveTask: taskConfig => - this.proxy.$resolveTask(handle, taskConfig).then(v => + this.proxy.$resolveTask(handle, this.fromTaskConfiguration(taskConfig)).then(v => this.toTaskConfiguration(v!) ) }; @@ -175,8 +175,16 @@ export class TasksMainImpl implements TasksMain, Disposable { protected toTaskConfiguration(taskDto: TaskDto): TaskConfiguration { return Object.assign(taskDto, { - _source: taskDto.source || 'plugin', + _source: taskDto.source, _scope: taskDto.scope }); } + + protected fromTaskConfiguration(task: TaskConfiguration): TaskDto { + return Object.assign(task, { + source: task._source, + scope: task._scope + }); + } + } diff --git a/packages/plugin-ext/src/plugin/type-converters.spec.ts b/packages/plugin-ext/src/plugin/type-converters.spec.ts index 80849efecde1b..c73badc37faae 100644 --- a/packages/plugin-ext/src/plugin/type-converters.spec.ts +++ b/packages/plugin-ext/src/plugin/type-converters.spec.ts @@ -183,7 +183,7 @@ describe('Type converters:', () => { type: shellType, label, source, - scope: undefined, + scope: 1, command, args, options: { cwd }, @@ -194,7 +194,7 @@ describe('Type converters:', () => { type: shellType, label, source, - scope: undefined, + scope: 2, command: commandLine, options: { cwd }, additionalProperty @@ -203,6 +203,7 @@ describe('Type converters:', () => { const shellPluginTask: theia.Task = { name: label, source, + scope: 1, definition: { type: shellType, additionalProperty @@ -219,6 +220,7 @@ describe('Type converters:', () => { const pluginTaskWithCommandLine: theia.Task = { name: label, source, + scope: 2, definition: { type: shellType, additionalProperty @@ -245,6 +247,7 @@ describe('Type converters:', () => { const customPluginTaskWithCommandLine: theia.Task = { name: label, source, + scope: 2, definition: { type: customType, additionalProperty diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 90e628a34db8c..219311f442413 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -708,7 +708,11 @@ export function fromTask(task: theia.Task): TaskDto | undefined { const taskDto = {} as TaskDto; taskDto.label = task.name; taskDto.source = task.source; - taskDto.scope = typeof task.scope === 'object' ? task.scope.uri.toString() : undefined; + if (typeof task.scope === 'object') { + taskDto.scope = task.scope.uri.toString(); + } else if (typeof task.scope === 'number') { + taskDto.scope = task.scope; + } const taskDefinition = task.definition; if (!taskDefinition) { @@ -748,13 +752,15 @@ export function toTask(taskDto: TaskDto): theia.Task { const result = {} as theia.Task; result.name = label; result.source = source; - if (scope) { + if (typeof scope === 'string') { const uri = URI.parse(scope); result.scope = { uri, name: uri.toString(), index: 0 }; + } else { + result.scope = scope; } const taskType = type; diff --git a/packages/preferences/src/browser/folder-preference-provider.ts b/packages/preferences/src/browser/folder-preference-provider.ts index fbb5c337fae63..cd460439a39df 100644 --- a/packages/preferences/src/browser/folder-preference-provider.ts +++ b/packages/preferences/src/browser/folder-preference-provider.ts @@ -17,40 +17,35 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { PreferenceScope } from '@theia/core/lib/browser'; -import { AbstractResourcePreferenceProvider } from './abstract-resource-preference-provider'; import { FileStat } from '@theia/filesystem/lib/common'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { SectionPreferenceProvider } from './section-preference-provider'; export const FolderPreferenceProviderFactory = Symbol('FolderPreferenceProviderFactory'); export interface FolderPreferenceProviderFactory { - (options: FolderPreferenceProviderOptions): FolderPreferenceProvider; + (uri: URI, section: string, folder: FileStat): FolderPreferenceProvider; } -export const FolderPreferenceProviderOptions = Symbol('FolderPreferenceProviderOptions'); +export const FolderPreferenceProviderFolder = Symbol('FolderPreferenceProviderFolder'); export interface FolderPreferenceProviderOptions { - readonly folder: FileStat; readonly configUri: URI; + readonly sectionName: string | undefined; } @injectable() -export class FolderPreferenceProvider extends AbstractResourcePreferenceProvider { +export class FolderPreferenceProvider extends SectionPreferenceProvider { @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - @inject(FolderPreferenceProviderOptions) protected readonly options: FolderPreferenceProviderOptions; + @inject(FolderPreferenceProviderFolder) protected readonly folder: FileStat; private _folderUri: URI; get folderUri(): URI { if (!this._folderUri) { - this._folderUri = new URI(this.options.folder.uri); + this._folderUri = new URI(this.folder.uri); } return this._folderUri; } - - protected getUri(): URI { - return this.options.configUri; - } - protected getScope(): PreferenceScope { if (!this.workspaceService.isMultiRootWorkspaceOpened) { // when FolderPreferenceProvider is used as a delegate of WorkspacePreferenceProvider in a one-folder workspace @@ -62,5 +57,4 @@ export class FolderPreferenceProvider extends AbstractResourcePreferenceProvider getDomain(): string[] { return [this.folderUri.toString()]; } - } diff --git a/packages/preferences/src/browser/folders-preferences-provider.ts b/packages/preferences/src/browser/folders-preferences-provider.ts index 48a3014decbd2..596972562eb25 100644 --- a/packages/preferences/src/browser/folders-preferences-provider.ts +++ b/packages/preferences/src/browser/folders-preferences-provider.ts @@ -21,7 +21,8 @@ import URI from '@theia/core/lib/common/uri'; import { PreferenceProvider, PreferenceResolveResult } from '@theia/core/lib/browser/preferences/preference-provider'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; -import { FolderPreferenceProvider, FolderPreferenceProviderFactory, FolderPreferenceProviderOptions } from './folder-preference-provider'; +import { FolderPreferenceProvider, FolderPreferenceProviderFactory } from './folder-preference-provider'; +import { FileStat } from '@theia/filesystem/lib/common'; @injectable() export class FoldersPreferencesProvider extends PreferenceProvider { @@ -57,12 +58,12 @@ export class FoldersPreferencesProvider extends PreferenceProvider { for (const folder of roots) { for (const configPath of this.configurations.getPaths()) { for (const configName of [...this.configurations.getSectionNames(), this.configurations.getConfigName()]) { - const configUri = this.configurations.createUri(new URI(folder.uri), configPath, configName); - const key = configUri.toString(); - toDelete.delete(key); - if (!this.providers.has(key)) { - const provider = this.createProvider({ folder, configUri }); - this.providers.set(key, provider); + const sectionUri = this.configurations.createUri(new URI(folder.uri), configPath, configName); + const sectionKey = sectionUri.toString(); + toDelete.delete(sectionKey); + if (!this.providers.has(sectionKey)) { + const provider = this.createProvider(sectionUri, configName, folder); + this.providers.set(sectionKey, provider); } } } @@ -202,6 +203,7 @@ export class FoldersPreferencesProvider extends PreferenceProvider { folderProviders.push(provider); providers.set(uri, folderProviders); + // in case we have nested folders mounted as workspace roots, select the innermost enclosing folder const relativity = provider.folderUri.path.relativity(resourcePath); if (relativity >= 0 && folder.relativity > relativity) { folder = { relativity, uri }; @@ -210,10 +212,13 @@ export class FoldersPreferencesProvider extends PreferenceProvider { return folder.uri && providers.get(folder.uri) || []; } - protected createProvider(options: FolderPreferenceProviderOptions): FolderPreferenceProvider { - const provider = this.folderPreferenceProviderFactory(options); + protected createProvider(uri: URI, section: string, folder: FileStat): FolderPreferenceProvider { + const provider = this.folderPreferenceProviderFactory(uri, section, folder); this.toDispose.push(provider); - this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change))); + this.toDispose.push(provider.onDidPreferencesChanged(change => { + this.onDidPreferencesChangedEmitter.fire(change); + } + )); return provider; } diff --git a/packages/preferences/src/browser/index.ts b/packages/preferences/src/browser/index.ts index 76ca1b008ea00..4ee9ecdda3899 100644 --- a/packages/preferences/src/browser/index.ts +++ b/packages/preferences/src/browser/index.ts @@ -20,3 +20,4 @@ export * from './user-preference-provider'; export * from './workspace-preference-provider'; export * from './folders-preferences-provider'; export * from './folder-preference-provider'; +export * from './user-configs-preference-provider'; diff --git a/packages/preferences/src/browser/preference-bindings.ts b/packages/preferences/src/browser/preference-bindings.ts index 1b6ff96c08d70..ace3ae54bd998 100644 --- a/packages/preferences/src/browser/preference-bindings.ts +++ b/packages/preferences/src/browser/preference-bindings.ts @@ -16,12 +16,13 @@ import { Container, interfaces } from 'inversify'; import { PreferenceProvider, PreferenceScope } from '@theia/core/lib/browser/preferences'; -import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; -import { UserPreferenceProvider } from './user-preference-provider'; +import { UserPreferenceProvider, UserPreferenceProviderFactory } from './user-preference-provider'; import { WorkspacePreferenceProvider } from './workspace-preference-provider'; import { WorkspaceFilePreferenceProvider, WorkspaceFilePreferenceProviderFactory, WorkspaceFilePreferenceProviderOptions } from './workspace-file-preference-provider'; import { FoldersPreferencesProvider } from './folders-preferences-provider'; -import { FolderPreferenceProvider, FolderPreferenceProviderFactory, FolderPreferenceProviderOptions } from './folder-preference-provider'; +import { FolderPreferenceProvider, FolderPreferenceProviderFactory, FolderPreferenceProviderFolder } from './folder-preference-provider'; +import { UserConfigsPreferenceProvider } from './user-configs-preference-provider'; +import { SectionPreferenceProviderUri, SectionPreferenceProviderSection } from './section-preference-provider'; export function bindWorkspaceFilePreferenceProvider(bind: interfaces.Bind): void { bind(WorkspaceFilePreferenceProviderFactory).toFactory(ctx => (options: WorkspaceFilePreferenceProviderOptions) => { @@ -33,30 +34,32 @@ export function bindWorkspaceFilePreferenceProvider(bind: interfaces.Bind): void }); } -export function bindFolderPreferenceProvider(bind: interfaces.Bind): void { - bind(FolderPreferenceProviderFactory).toFactory(ctx => - (options: FolderPreferenceProviderOptions) => { +export function bindFactory(bind: interfaces.Bind, + factoryId: interfaces.ServiceIdentifier, + constructor: interfaces.Newable, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...parameterBindings: interfaces.ServiceIdentifier[]): void { + bind(factoryId).toFactory(ctx => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (...args: any[]) => { const child = new Container({ defaultScope: 'Singleton' }); child.parent = ctx.container; - child.bind(FolderPreferenceProviderOptions).toConstantValue(options); - const configurations = ctx.container.get(PreferenceConfigurations); - if (configurations.isConfigUri(options.configUri)) { - child.bind(FolderPreferenceProvider).toSelf(); - return child.get(FolderPreferenceProvider); + for (let i = 0; i < parameterBindings.length; i++) { + child.bind(parameterBindings[i]).toConstantValue(args[i]); } - const sectionName = configurations.getName(options.configUri); - return child.getNamed(FolderPreferenceProvider, sectionName); + child.bind(constructor).to(constructor); + return child.get(constructor); } ); - } export function bindPreferenceProviders(bind: interfaces.Bind, unbind: interfaces.Unbind): void { unbind(PreferenceProvider); - bind(PreferenceProvider).to(UserPreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.User); + bind(PreferenceProvider).to(UserConfigsPreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.User); bind(PreferenceProvider).to(WorkspacePreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Workspace); bind(PreferenceProvider).to(FoldersPreferencesProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Folder); - bindFolderPreferenceProvider(bind); bindWorkspaceFilePreferenceProvider(bind); + bindFactory(bind, UserPreferenceProviderFactory, UserPreferenceProvider, SectionPreferenceProviderUri, SectionPreferenceProviderSection); + bindFactory(bind, FolderPreferenceProviderFactory, FolderPreferenceProvider, SectionPreferenceProviderUri, SectionPreferenceProviderSection, FolderPreferenceProviderFolder); } diff --git a/packages/preferences/src/browser/section-preference-provider.ts b/packages/preferences/src/browser/section-preference-provider.ts new file mode 100644 index 0000000000000..6e9c52d76f8dc --- /dev/null +++ b/packages/preferences/src/browser/section-preference-provider.ts @@ -0,0 +1,83 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { AbstractResourcePreferenceProvider } from './abstract-resource-preference-provider'; +import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; + +export const SectionPreferenceProviderUri = Symbol('SectionPreferenceProviderUri'); +export const SectionPreferenceProviderSection = Symbol('SectionPreferenceProviderSection'); + +/** + * This class encapsulates the logic of using separate files for some workpace configuration like 'launch.json' or 'tasks.json'. + * Anything that is not a contributed section will be in the main config file. + */ +@injectable() +export abstract class SectionPreferenceProvider extends AbstractResourcePreferenceProvider { + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + @inject(SectionPreferenceProviderUri) + protected readonly uri: URI; + @inject(SectionPreferenceProviderSection) + protected readonly section: string; + @inject(PreferenceConfigurations) + protected readonly preferenceConfigurations: PreferenceConfigurations; + + private _isSection?: boolean; + + private get isSection(): boolean { + if (typeof this._isSection === 'undefined') { + this._isSection = this.preferenceConfigurations.isSectionName(this.section); + } + return this._isSection; + } + + protected getUri(): URI { + return this.uri; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected parse(content: string): any { + const prefs = super.parse(content); + if (this.isSection) { + if (prefs === undefined) { + return undefined; + } + const result: { [k: string]: unknown } = { + + }; + result[this.section] = { ...prefs }; + return result; + } else { + return prefs; + } + } + + protected getPath(preferenceName: string): string[] | undefined { + if (!this.isSection) { + return super.getPath(preferenceName); + } + if (preferenceName === this.section) { + return []; + } + if (preferenceName.startsWith(this.section + '.')) { + return [preferenceName.substr(this.section!.length + 1)]; + } + return undefined; + } +} diff --git a/packages/preferences/src/browser/user-configs-preference-provider.ts b/packages/preferences/src/browser/user-configs-preference-provider.ts new file mode 100644 index 0000000000000..de7998de62f17 --- /dev/null +++ b/packages/preferences/src/browser/user-configs-preference-provider.ts @@ -0,0 +1,115 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { inject, injectable, postConstruct } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { PreferenceProvider, PreferenceResolveResult } from '@theia/core/lib/browser/preferences/preference-provider'; +import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +import { UserStorageUri } from '@theia/userstorage/lib/browser'; +import { UserPreferenceProvider, UserPreferenceProviderFactory } from './user-preference-provider'; + +export const USER_PREFERENCE_FOLDER = new URI().withScheme(UserStorageUri.SCHEME); + +/** + * Binds together preference section prefs providers for user-level preferences. + */ +@injectable() +export class UserConfigsPreferenceProvider extends PreferenceProvider { + + @inject(UserPreferenceProviderFactory) + protected readonly providerFactory: UserPreferenceProviderFactory; + + @inject(PreferenceConfigurations) + protected readonly configurations: PreferenceConfigurations; + + protected readonly providers = new Map(); + + @postConstruct() + protected async init(): Promise { + this.createProviders(); + + const readyPromises: Promise[] = []; + for (const provider of this.providers.values()) { + readyPromises.push(provider.ready.catch(e => console.error(e))); + } + Promise.all(readyPromises).then(() => this._ready.resolve()); + } + + protected createProviders(): void { + for (const configName of [...this.configurations.getSectionNames(), this.configurations.getConfigName()]) { + const sectionUri = USER_PREFERENCE_FOLDER.withPath(configName + '.json'); + const sectionKey = sectionUri.toString(); + if (!this.providers.has(sectionKey)) { + const provider = this.createProvider(sectionUri, configName); + this.providers.set(sectionKey, provider); + } + } + } + + getConfigUri(resourceUri?: string): URI | undefined { + for (const provider of this.providers.values()) { + const configUri = provider.getConfigUri(resourceUri); + if (this.configurations.isConfigUri(configUri)) { + return configUri; + } + } + return undefined; + } + + resolve(preferenceName: string, resourceUri?: string): PreferenceResolveResult { + const result: PreferenceResolveResult = {}; + for (const provider of this.providers.values()) { + const { value, configUri } = provider.resolve(preferenceName, resourceUri); + if (configUri && value !== undefined) { + result.configUri = configUri; + result.value = PreferenceProvider.merge(result.value as any, value as any) as any; + } + } + return result; + } + + getPreferences(resourceUri?: string): { [p: string]: any } { + let result = {}; + for (const provider of this.providers.values()) { + const preferences = provider.getPreferences(); + result = PreferenceProvider.merge(result, preferences) as any; + } + return result; + } + + async setPreference(preferenceName: string, value: any, resourceUri?: string): Promise { + const sectionName = preferenceName.split('.', 1)[0]; + const configName = this.configurations.isSectionName(sectionName) ? sectionName : this.configurations.getConfigName(); + + const providers = this.providers.values(); + + for (const provider of providers) { + if (this.configurations.getName(provider.getConfigUri()) === configName) { + return provider.setPreference(preferenceName, value, resourceUri); + } + } + return false; + } + + protected createProvider(uri: URI, sectionName: string): UserPreferenceProvider { + const provider = this.providerFactory(uri, sectionName); + this.toDispose.push(provider); + this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change))); + return provider; + } +} diff --git a/packages/preferences/src/browser/user-preference-provider.ts b/packages/preferences/src/browser/user-preference-provider.ts index d5a186d7e3359..4e12f864e5c29 100644 --- a/packages/preferences/src/browser/user-preference-provider.ts +++ b/packages/preferences/src/browser/user-preference-provider.ts @@ -16,18 +16,22 @@ import { injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; -import { AbstractResourcePreferenceProvider } from './abstract-resource-preference-provider'; import { UserStorageUri } from '@theia/userstorage/lib/browser'; import { PreferenceScope } from '@theia/core/lib/browser'; +import { SectionPreferenceProvider } from './section-preference-provider'; export const USER_PREFERENCE_URI = new URI().withScheme(UserStorageUri.SCHEME).withPath('settings.json'); -@injectable() -export class UserPreferenceProvider extends AbstractResourcePreferenceProvider { - protected getUri(): URI { - return USER_PREFERENCE_URI; - } +export const UserPreferenceProviderFactory = Symbol('UserPreferenceProviderFactory'); +export interface UserPreferenceProviderFactory { + (uri: URI, section: string): UserPreferenceProvider; +}; +/** + * A @SectionPreferenceProvider that targets the user-level settings + */ +@injectable() +export class UserPreferenceProvider extends SectionPreferenceProvider { protected getScope(): PreferenceScope { return PreferenceScope.User; } diff --git a/packages/task/src/browser/process/process-task-resolver.ts b/packages/task/src/browser/process/process-task-resolver.ts index d5070178b1bae..7ca040fc86f2e 100644 --- a/packages/task/src/browser/process/process-task-resolver.ts +++ b/packages/task/src/browser/process/process-task-resolver.ts @@ -21,6 +21,7 @@ import { TaskConfiguration } from '../../common/task-protocol'; import { ProcessTaskConfiguration } from '../../common/process/task-protocol'; import { TaskDefinitionRegistry } from '../task-definition-registry'; import URI from '@theia/core/lib/common/uri'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; @injectable() export class ProcessTaskResolver implements TaskResolver { @@ -31,6 +32,9 @@ export class ProcessTaskResolver implements TaskResolver { @inject(TaskDefinitionRegistry) protected readonly taskDefinitionRegistry: TaskDefinitionRegistry; + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + /** * Perform some adjustments to the task launch configuration, before sending * it to the backend to be executed. We can make sure that parameters that @@ -41,11 +45,19 @@ export class ProcessTaskResolver implements TaskResolver { if (taskConfig.type !== 'process' && taskConfig.type !== 'shell') { throw new Error('Unsupported task configuration type.'); } - const context = new URI(this.taskDefinitionRegistry.getDefinition(taskConfig) ? taskConfig.scope : taskConfig._source).withScheme('file'); + const context = typeof taskConfig._scope === 'string' ? new URI(taskConfig._scope) : undefined; const variableResolverOptions = { context, configurationSection: 'tasks' }; const processTaskConfig = taskConfig as ProcessTaskConfiguration; + let cwd = processTaskConfig.options && processTaskConfig.options.cwd; + if (!cwd) { + const rootURI = this.workspaceService.getWorkspaceRootUri(context); + if (rootURI) { + cwd = rootURI.toString(); + } + } + const result: ProcessTaskConfiguration = { ...processTaskConfig, command: await this.variableResolverService.resolve(processTaskConfig.command, variableResolverOptions), @@ -66,7 +78,7 @@ export class ProcessTaskResolver implements TaskResolver { options: processTaskConfig.linux.options } : undefined, options: { - cwd: await this.variableResolverService.resolve(processTaskConfig.options && processTaskConfig.options.cwd || '${workspaceFolder}', variableResolverOptions), + cwd: await this.variableResolverService.resolve(cwd, variableResolverOptions), env: processTaskConfig.options && processTaskConfig.options.env, shell: processTaskConfig.options && processTaskConfig.options.shell } diff --git a/packages/task/src/browser/provided-task-configurations.ts b/packages/task/src/browser/provided-task-configurations.ts index cdf27e1f7032c..9f44546a0a898 100644 --- a/packages/task/src/browser/provided-task-configurations.ts +++ b/packages/task/src/browser/provided-task-configurations.ts @@ -17,8 +17,7 @@ import { inject, injectable } from 'inversify'; import { TaskProviderRegistry } from './task-contribution'; import { TaskDefinitionRegistry } from './task-definition-registry'; -import { TaskConfiguration, TaskCustomization, TaskOutputPresentation } from '../common'; -import URI from '@theia/core/lib/common/uri'; +import { TaskConfiguration, TaskCustomization, TaskOutputPresentation, TaskConfigurationScope } from '../common'; @injectable() export class ProvidedTaskConfigurations { @@ -56,7 +55,7 @@ export class ProvidedTaskConfigurations { } /** returns the task configuration for a given source and label or undefined if none */ - async getTask(source: string, taskLabel: string, scope?: string): Promise { + async getTask(source: string, taskLabel: string, scope: TaskConfigurationScope): Promise { const task = this.getCachedTask(source, taskLabel, scope); if (task) { return task; @@ -74,7 +73,7 @@ export class ProvidedTaskConfigurations { * @param customization the task customization * @return the detected task for the given task customization. If the task customization is not found, `undefined` is returned. */ - async getTaskToCustomize(customization: TaskCustomization, rootFolderPath: string): Promise { + async getTaskToCustomize(customization: TaskCustomization, scope: TaskConfigurationScope): Promise { const definition = this.taskDefinitionRegistry.getDefinition(customization); if (!definition) { return undefined; @@ -104,20 +103,19 @@ export class ProvidedTaskConfigurations { // find the task that matches the `customization`. // The scenario where more than one match is found should not happen unless users manually enter multiple customizations for one type of task // If this does happen, return the first match - const rootFolderUri = new URI(rootFolderPath).toString(); const matchedTask = matchedTasks.filter(t => - rootFolderUri === t._scope && definition.properties.all.every(p => t[p] === customization[p]) + scope === t._scope && definition.properties.all.every(p => t[p] === customization[p]) )[0]; return matchedTask; } - protected getCachedTask(source: string, taskLabel: string, scope?: string): TaskConfiguration | undefined { + protected getCachedTask(source: string, taskLabel: string, scope?: TaskConfigurationScope): TaskConfiguration | undefined { const labelConfigMap = this.tasksMap.get(source); if (labelConfigMap) { const scopeConfigMap = labelConfigMap.get(taskLabel); if (scopeConfigMap) { if (scope) { - return scopeConfigMap.get(scope); + return scopeConfigMap.get(scope.toString()); } return Array.from(scopeConfigMap.values())[0]; } @@ -132,16 +130,16 @@ export class ProvidedTaskConfigurations { if (this.tasksMap.has(source)) { const labelConfigMap = this.tasksMap.get(source)!; if (labelConfigMap.has(label)) { - labelConfigMap.get(label)!.set(scope, task); + labelConfigMap.get(label)!.set(scope.toString(), task); } else { const newScopeConfigMap = new Map(); - newScopeConfigMap.set(scope, task); + newScopeConfigMap.set(scope.toString(), task); labelConfigMap.set(label, newScopeConfigMap); } } else { const newLabelConfigMap = new Map>(); const newScopeConfigMap = new Map(); - newScopeConfigMap.set(scope, task); + newScopeConfigMap.set(scope.toString(), task); newLabelConfigMap.set(label, newScopeConfigMap); this.tasksMap.set(source, newLabelConfigMap); } diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index 98da9ad7c48ab..ecd5197954db4 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -16,7 +16,7 @@ import { inject, injectable } from 'inversify'; import { TaskService } from './task-service'; -import { TaskInfo, TaskConfiguration, TaskCustomization } from '../common/task-protocol'; +import { TaskInfo, TaskConfiguration, TaskCustomization, TaskScope, TaskConfigurationScope } from '../common/task-protocol'; import { TaskDefinitionRegistry } from './task-definition-registry'; import URI from '@theia/core/lib/common/uri'; import { QuickOpenHandler, QuickOpenService, QuickOpenOptions, QuickOpenBaseAction, LabelProvider } from '@theia/core/lib/browser'; @@ -335,7 +335,7 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { if (defaultBuildOrTestTasks.length === 1) { // run the default build / test task const defaultBuildOrTestTask = defaultBuildOrTestTasks[0]; const taskToRun = (defaultBuildOrTestTask as TaskRunQuickOpenItem).getTask(); - const scope = this.taskSourceResolver.resolve(taskToRun); + const scope = taskToRun._scope; if (this.taskDefinitionRegistry && !!this.taskDefinitionRegistry.getDefinition(taskToRun)) { this.taskService.run(taskToRun.source, taskToRun.label, scope); @@ -451,10 +451,10 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { const grouped = new Map(); for (const task of tasks) { const folder = task._scope; - if (grouped.has(folder)) { - grouped.get(folder)!.push(task); + if (grouped.has(folder.toString())) { + grouped.get(folder.toString())!.push(task); } else { - grouped.set(folder, [task]); + grouped.set(folder.toString(), [task]); } } for (const taskConfigs of grouped.values()) { @@ -491,18 +491,7 @@ export class TaskRunQuickOpenItem extends QuickOpenGroupItem { } getDescription(): string { - if (!this.isMulti) { - return ''; - } - if (this.taskDefinitionRegistry && !!this.taskDefinitionRegistry.getDefinition(this.task)) { - if (this.task._scope) { - return new URI(this.task._scope).displayName; - } - return this.task._source; - } else { - return new URI(this.task._source).displayName; - } - + return renderScope(this.task._scope, this.isMulti); } run(mode: QuickOpenMode): boolean { @@ -510,7 +499,7 @@ export class TaskRunQuickOpenItem extends QuickOpenGroupItem { return false; } - const scope = this.taskSourceResolver.resolve(this.task); + const scope = this.task._scope; if (this.taskDefinitionRegistry && !!this.taskDefinitionRegistry.getDefinition(this.task)) { this.taskService.run(this.task.source || this.task._source, this.task.label, scope); } else { @@ -549,6 +538,19 @@ export class ConfigureBuildOrTestTaskQuickOpenItem extends TaskRunQuickOpenItem } } +function renderScope(scope: TaskConfigurationScope, isMulti: boolean): string { + if (typeof scope === 'string') { + if (isMulti) { + return new URI(scope).displayName; + } else { + return ''; + } + } else { + return TaskScope[scope]; + } + +} + export class TaskConfigureQuickOpenItem extends QuickOpenGroupItem { protected taskDefinitionRegistry: TaskDefinitionRegistry; @@ -575,17 +577,7 @@ export class TaskConfigureQuickOpenItem extends QuickOpenGroupItem { } getDescription(): string { - if (!this.isMulti) { - return ''; - } - if (this.taskDefinitionRegistry && !!this.taskDefinitionRegistry.getDefinition(this.task)) { - if (this.task._scope) { - return new URI(this.task._scope).displayName; - } - return this.task._source; - } else { - return new URI(this.task._source).displayName; - } + return renderScope(this.task._scope, this.isMulti); } run(mode: QuickOpenMode): boolean { @@ -775,11 +767,7 @@ export class RunningTaskQuickOpenItem extends QuickOpenItem { } getDescription(): string { - if (!this.isMulti) { - return ''; - } - const source = this.taskSourceResolver.resolve(this.taskInfo.config); - return source ? this.labelProvider.getName(new URI(source)) : ''; + return renderScope(this.taskInfo.config._scope, this.isMulti); } run(mode: QuickOpenMode): boolean { diff --git a/packages/task/src/browser/task-configuration-manager.ts b/packages/task/src/browser/task-configuration-manager.ts index b4fed15ea5a43..d882592659e95 100644 --- a/packages/task/src/browser/task-configuration-manager.ts +++ b/packages/task/src/browser/task-configuration-manager.ts @@ -15,21 +15,29 @@ ********************************************************************************/ import debounce = require('p-debounce'); -import { inject, injectable, postConstruct } from 'inversify'; +import { inject, injectable, postConstruct, named } 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, PreferenceScope } from '@theia/core/lib/browser'; +import { PreferenceScope, PreferenceProvider } 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 { TaskCustomization, TaskConfiguration, TaskConfigurationScope, TaskScope } from '../common/task-protocol'; import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common'; -import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; +import { FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +export interface TasksChange { + scope: TaskConfigurationScope; + type: FileChangeType; +} +/** + * This class connnects the the "tasks" preferences sections to task system: it collects tasks preference values and + * provides them to the task system as raw, parsed JSON. + */ @injectable() export class TaskConfigurationManager { @@ -45,8 +53,11 @@ export class TaskConfigurationManager { @inject(FileSystem) protected readonly filesystem: FileSystem; - @inject(PreferenceService) - protected readonly preferences: PreferenceService; + @inject(PreferenceProvider) @named(PreferenceScope.Folder) + protected readonly folderPreferences: PreferenceProvider; + + @inject(PreferenceProvider) @named(PreferenceScope.User) + protected readonly userPreferences: PreferenceProvider; @inject(PreferenceConfigurations) protected readonly preferenceConfigurations: PreferenceConfigurations; @@ -57,14 +68,19 @@ export class TaskConfigurationManager { @inject(TaskTemplateSelector) protected readonly taskTemplateSelector: TaskTemplateSelector; - protected readonly onDidChangeTaskConfigEmitter = new Emitter(); - readonly onDidChangeTaskConfig: Event = this.onDidChangeTaskConfigEmitter.event; + protected readonly onDidChangeTaskConfigEmitter = new Emitter(); + readonly onDidChangeTaskConfig: Event = this.onDidChangeTaskConfigEmitter.event; + + protected readonly models = new Map(); + protected userModel: TaskConfigurationModel; @postConstruct() protected async init(): Promise { + this.userModel = new TaskConfigurationModel(TaskScope.Global, this.userPreferences); + this.userModel.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ scope: TaskScope.Global, type: FileChangeType.UPDATED })); this.updateModels(); - this.preferences.onPreferenceChanged(e => { - if (e.preferenceName === 'tasks') { + this.folderPreferences.onDidPreferencesChanged(e => { + if (e['tasks']) { this.updateModels(); } }); @@ -73,7 +89,6 @@ export class TaskConfigurationManager { }); } - protected readonly models = new Map(); protected updateModels = debounce(async () => { const roots = await this.workspaceService.roots; const toDelete = new Set(this.models.keys()); @@ -81,11 +96,11 @@ export class TaskConfigurationManager { const key = rootStat.uri; toDelete.delete(key); if (!this.models.has(key)) { - const model = new TaskConfigurationModel(key, this.preferences); - model.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ uri: key, type: FileChangeType.UPDATED })); + const model = new TaskConfigurationModel(key, this.folderPreferences); + model.onDidChange(() => this.onDidChangeTaskConfigEmitter.fire({ scope: key, type: FileChangeType.UPDATED })); model.onDispose(() => this.models.delete(key)); this.models.set(key, model); - this.onDidChangeTaskConfigEmitter.fire({ uri: key, type: FileChangeType.UPDATED }); + this.onDidChangeTaskConfigEmitter.fire({ scope: key, type: FileChangeType.UPDATED }); } } for (const uri of toDelete) { @@ -93,20 +108,20 @@ export class TaskConfigurationManager { if (model) { model.dispose(); } - this.onDidChangeTaskConfigEmitter.fire({ uri, type: FileChangeType.DELETED }); + this.onDidChangeTaskConfigEmitter.fire({ scope: uri, type: FileChangeType.DELETED }); } }, 500); - getTasks(sourceFolderUri: string): (TaskCustomization | TaskConfiguration)[] { - if (this.models.has(sourceFolderUri)) { - const taskPrefModel = this.models.get(sourceFolderUri)!; + getTasks(scope: TaskConfigurationScope): (TaskCustomization | TaskConfiguration)[] { + if (typeof scope === 'string' && this.models.has(scope)) { + const taskPrefModel = this.models.get(scope)!; return taskPrefModel.configurations; } - return []; + return this.userModel.configurations; } - getTask(name: string, sourceFolderUri: string | undefined): TaskCustomization | TaskConfiguration | undefined { - const taskPrefModel = this.getModel(sourceFolderUri); + getTask(name: string, scope: TaskConfigurationScope): TaskCustomization | TaskConfiguration | undefined { + const taskPrefModel = this.getModel(scope); if (taskPrefModel) { for (const configuration of taskPrefModel.configurations) { if (configuration.name === name) { @@ -114,39 +129,45 @@ export class TaskConfigurationManager { } } } + return this.userModel.configurations.find(configuration => configuration.name === 'name'); } - async openConfiguration(sourceFolderUri: string): Promise { - const taskPrefModel = this.getModel(sourceFolderUri); + async openConfiguration(scope: TaskConfigurationScope): Promise { + const taskPrefModel = this.getModel(scope); if (taskPrefModel) { await this.doOpen(taskPrefModel); } } - async addTaskConfiguration(sourceFolderUri: string, taskConfig: TaskCustomization): Promise { + async addTaskConfiguration(sourceFolderUri: string, taskConfig: TaskCustomization): Promise { const taskPrefModel = this.getModel(sourceFolderUri); if (taskPrefModel) { const configurations = taskPrefModel.configurations; return this.setTaskConfigurations(sourceFolderUri, [...configurations, taskConfig]); } + return false; } - async setTaskConfigurations(sourceFolderUri: string, taskConfigs: (TaskCustomization | TaskConfiguration)[]): Promise { + async setTaskConfigurations(sourceFolderUri: string, taskConfigs: (TaskCustomization | TaskConfiguration)[]): Promise { const taskPrefModel = this.getModel(sourceFolderUri); if (taskPrefModel) { return taskPrefModel.setConfigurations(taskConfigs); } + return false; } - private getModel(sourceFolderUri: string | undefined): TaskConfigurationModel | undefined { - if (!sourceFolderUri) { + private getModel(scope: TaskConfigurationScope): TaskConfigurationModel | undefined { + if (!scope) { return undefined; } for (const model of this.models.values()) { - if (model.workspaceFolderUri === sourceFolderUri) { + if (model.scope === scope) { return model; } } + if (scope === TaskScope.Global) { + return this.userModel; + } } protected async doOpen(model: TaskConfigurationModel): Promise { @@ -164,14 +185,14 @@ export class TaskConfigurationManager { protected async doCreate(model: TaskConfigurationModel): Promise { 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 + await this.folderPreferences.setPreference('tasks', {}, model.getWorkspaceFolder()); // create dummy tasks.json in the correct place + const { configUri } = this.folderPreferences.resolve('tasks', model.getWorkspaceFolder()); // 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`); + uri = new URI(model.getWorkspaceFolder()).resolve(`${this.preferenceConfigurations.getPaths()[0]}/tasks.json`); } const fileStat = await this.filesystem.getFileStat(uri.toString()); diff --git a/packages/task/src/browser/task-configuration-model.ts b/packages/task/src/browser/task-configuration-model.ts index 57d8dad7e3905..4e61995194409 100644 --- a/packages/task/src/browser/task-configuration-model.ts +++ b/packages/task/src/browser/task-configuration-model.ts @@ -17,9 +17,13 @@ import URI from '@theia/core/lib/common/uri'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { TaskCustomization, TaskConfiguration } from '../common/task-protocol'; -import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser/preferences/preference-service'; +import { TaskCustomization, TaskConfiguration, TaskConfigurationScope } from '../common/task-protocol'; +import { PreferenceProvider, PreferenceProviderDataChanges, PreferenceProviderDataChange } from '@theia/core/lib/browser'; +/** + * Holds the task configurations associated with a particular file. Uses an editor model to facilitate + * non-destructive editing and coordination with editing the file by hand. + */ export class TaskConfigurationModel implements Disposable { protected json: TaskConfigurationModel.JsonContent; @@ -32,12 +36,13 @@ export class TaskConfigurationModel implements Disposable { ); constructor( - public readonly workspaceFolderUri: string, - protected readonly preferences: PreferenceService + public readonly scope: TaskConfigurationScope, + protected readonly preferences: PreferenceProvider ) { this.reconcile(); - this.toDispose.push(this.preferences.onPreferenceChanged(e => { - if (e.preferenceName === 'tasks' && e.affects(workspaceFolderUri)) { + this.toDispose.push(this.preferences.onDidPreferencesChanged((e: PreferenceProviderDataChanges) => { + const change = e['tasks']; + if (change && PreferenceProviderDataChange.affects(change, this.getWorkspaceFolder())) { this.reconcile(); } })); @@ -47,6 +52,10 @@ export class TaskConfigurationModel implements Disposable { return this.json.uri; } + getWorkspaceFolder(): string | undefined { + return typeof this.scope === 'string' ? this.scope : undefined; + } + dispose(): void { this.toDispose.dispose(); } @@ -63,14 +72,14 @@ export class TaskConfigurationModel implements Disposable { this.onDidChangeEmitter.fire(undefined); } - setConfigurations(value: object): Promise { - return this.preferences.set('tasks.tasks', value, PreferenceScope.Folder, this.workspaceFolderUri); + setConfigurations(value: object): Promise { + return this.preferences.setPreference('tasks.tasks', value, this.getWorkspaceFolder()); } protected parseConfigurations(): TaskConfigurationModel.JsonContent { const configurations: (TaskCustomization | TaskConfiguration)[] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { configUri, value } = this.preferences.resolve('tasks', undefined, this.workspaceFolderUri); + const { configUri, value } = this.preferences.resolve('tasks', this.getWorkspaceFolder()); if (value && typeof value === 'object' && 'tasks' in value) { if (Array.isArray(value.tasks)) { for (const taskConfig of value.tasks) { diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index fa089f4097716..8917a490d7a7c 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -16,22 +16,23 @@ import { inject, injectable, postConstruct } from 'inversify'; import { - ContributedTaskConfiguration, TaskConfiguration, TaskCustomization, TaskDefinition, - TaskOutputPresentation + TaskOutputPresentation, + TaskConfigurationScope, + TaskScope } from '../common'; import { TaskDefinitionRegistry } from './task-definition-registry'; import { ProvidedTaskConfigurations } from './provided-task-configurations'; -import { TaskConfigurationManager } from './task-configuration-manager'; +import { TaskConfigurationManager, TasksChange } from './task-configuration-manager'; import { TaskSchemaUpdater } from './task-schema-updater'; import { TaskSourceResolver } from './task-source-resolver'; import { Disposable, DisposableCollection } from '@theia/core/lib/common'; -import URI from '@theia/core/lib/common/uri'; -import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; +import { FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { OpenerService } from '@theia/core/lib/browser'; +import { USER_PREFERENCE_FOLDER } from '@theia/preferences/lib/browser/user-configs-preference-provider'; export interface TaskConfigurationClient { /** @@ -41,12 +42,13 @@ export interface TaskConfigurationClient { taskConfigurationChanged: (event: string[]) => void; } +export const USER_TASKS_URI = USER_PREFERENCE_FOLDER.withPath('tasks.json'); + /** * Watches a tasks.json configuration file and provides a parsed version of the contained task configurations */ @injectable() export class TaskConfigurations implements Disposable { - protected readonly toDispose = new DisposableCollection(); /** * Map of source (path of root folder that the task configs come from) and task config map. @@ -139,8 +141,10 @@ export class TaskConfigurations implements Disposable { const detectedTasksAsConfigured: TaskConfiguration[] = []; for (const [rootFolder, customizations] of Array.from(this.taskCustomizationMap.entries())) { for (const cus of customizations) { + // TODO: getTasksToCustomize() will ask all task providers to contribute tasks. Doing this in a loop is bad. const detected = await this.providedTaskConfigurations.getTaskToCustomize(cus, rootFolder); if (detected) { + // there might be a provided task that has a different scope from the task we're inspecting detectedTasksAsConfigured.push({ ...detected, ...cus }); } } @@ -148,16 +152,18 @@ export class TaskConfigurations implements Disposable { return [...configuredTasks, ...detectedTasksAsConfigured]; } - getRawTaskConfigurations(rootFolder?: string): (TaskCustomization | TaskConfiguration)[] { - if (!rootFolder) { + getRawTaskConfigurations(scope?: TaskConfigurationScope): (TaskCustomization | TaskConfiguration)[] { + if (!scope) { const tasks: (TaskCustomization | TaskConfiguration)[] = []; for (const configs of this.rawTaskConfigurations.values()) { tasks.push(...configs); } return tasks; } - if (this.rawTaskConfigurations.has(rootFolder)) { - return Array.from(this.rawTaskConfigurations.get(rootFolder)!.values()); + + const scopeKey = this.getKeyFromScope(scope); + if (this.rawTaskConfigurations.has(scopeKey)) { + return Array.from(this.rawTaskConfigurations.get(scopeKey)!.values()); } return []; } @@ -179,20 +185,20 @@ export class TaskConfigurations implements Disposable { } /** returns the task configuration for a given label or undefined if none */ - getTask(rootFolderPath: string, taskLabel: string): TaskConfiguration | undefined { - const labelConfigMap = this.tasksMap.get(rootFolderPath); + getTask(scope: TaskConfigurationScope, taskLabel: string): TaskConfiguration | undefined { + const labelConfigMap = this.tasksMap.get(this.getKeyFromScope(scope)); if (labelConfigMap) { return labelConfigMap.get(taskLabel); } } /** returns the customized task for a given label or undefined if none */ - async getCustomizedTask(rootFolderPath: string, taskLabel: string): Promise { - const customizations = this.taskCustomizationMap.get(rootFolderPath); + async getCustomizedTask(scope: TaskConfigurationScope, taskLabel: string): Promise { + const customizations = this.taskCustomizationMap.get(this.getKeyFromScope(scope)); if (customizations) { const customization = customizations.find(cus => cus.label === taskLabel); if (customization) { - const detected = await this.providedTaskConfigurations.getTaskToCustomize(customization, rootFolderPath); + const detected = await this.providedTaskConfigurations.getTaskToCustomize(customization, scope); if (detected) { return { ...detected, @@ -205,8 +211,8 @@ export class TaskConfigurations implements Disposable { } /** removes tasks configured in the given task config file */ - private removeTasks(configFileUri: string): void { - const source = this.getSourceFolderFromConfigUri(configFileUri); + private removeTasks(scope: TaskConfigurationScope): void { + const source = this.getKeyFromScope(scope); this.tasksMap.delete(source); this.taskCustomizationMap.delete(source); } @@ -215,8 +221,8 @@ export class TaskConfigurations implements Disposable { * Removes task customization objects found in the given task config file from the memory. * Please note: this function does not modify the task config file. */ - private removeTaskCustomizations(configFileUri: string): void { - const source = this.getSourceFolderFromConfigUri(configFileUri); + private removeTaskCustomizations(scope: TaskConfigurationScope): void { + const source = this.getKeyFromScope(scope); this.taskCustomizationMap.delete(source); } @@ -225,16 +231,13 @@ export class TaskConfigurations implements Disposable { * @param type the type of task customizations * @param rootFolder the root folder to find task customizations from. If `undefined`, this function returns an empty array. */ - getTaskCustomizations(type: string, rootFolder?: string): TaskCustomization[] { - if (!rootFolder) { - return []; - } - - const customizationInRootFolder = this.taskCustomizationMap.get(new URI(rootFolder).toString()); + private getTaskCustomizations(type: string, scope: TaskConfigurationScope): TaskCustomization[] { + const customizationInRootFolder = this.taskCustomizationMap.get(this.getKeyFromScope(scope)); if (customizationInRootFolder) { return customizationInRootFolder.filter(c => c.type === type); + } else { + return []; } - return []; } /** @@ -265,13 +268,13 @@ export class TaskConfigurations implements Disposable { /** * Called when a change, to a config file we watch, is detected. */ - protected async onDidTaskFileChange(fileChanges: FileChange[]): Promise { + protected async onDidTaskFileChange(fileChanges: TasksChange[]): Promise { for (const change of fileChanges) { if (change.type === FileChangeType.DELETED) { - this.removeTasks(change.uri); + this.removeTasks(change.scope); } else { // re-parse the config file - await this.refreshTasks(change.uri); + await this.refreshTasks(change.scope); } } } @@ -279,47 +282,55 @@ export class TaskConfigurations implements Disposable { /** * Read the task configs from the task configuration manager, and updates the list of available tasks. */ - protected async refreshTasks(rootFolderUri: string): Promise { - await this.readTasks(rootFolderUri); + protected async refreshTasks(scope: TaskConfigurationScope): Promise { + await this.readTasks(scope); - this.removeTasks(rootFolderUri); - this.removeTaskCustomizations(rootFolderUri); + this.removeTasks(scope); + this.removeTaskCustomizations(scope); this.reorganizeTasks(); } /** parses a config file and extracts the tasks launch configurations */ - protected async readTasks(rootFolderUri: string): Promise<(TaskCustomization | TaskConfiguration)[] | undefined> { - const rawConfigArray = this.taskConfigurationManager.getTasks(rootFolderUri); - if (this.rawTaskConfigurations.has(rootFolderUri)) { - this.rawTaskConfigurations.delete(rootFolderUri); + protected async readTasks(scope: TaskConfigurationScope): Promise<(TaskCustomization | TaskConfiguration)[] | undefined> { + const rawConfigArray = this.taskConfigurationManager.getTasks(scope); + const key = this.getKeyFromScope(scope); + if (this.rawTaskConfigurations.has(key)) { + this.rawTaskConfigurations.delete(key); } - this.rawTaskConfigurations.set(rootFolderUri, rawConfigArray); + this.rawTaskConfigurations.set(key, rawConfigArray); return rawConfigArray; } + async openUserTasks(): Promise { + await this.openerService.getOpener(USER_TASKS_URI).then(opener => opener.open(USER_TASKS_URI)); + } + /** 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) { + const scope = task._scope; + if (scope === TaskScope.Global) { + return this.openUserTasks(); + } else if (typeof scope !== 'string') { + console.error('Global task cannot be customized'); + // TODO detected tasks of scope workspace or user could be customized in those preferences. return; } - const sourceFolderUri: string | undefined = this.taskSourceResolver.resolve(task); - if (!sourceFolderUri) { - console.error('Global task cannot be customized'); + const workspace = this.workspaceService.workspace; + if (!workspace) { return; } const configuredAndCustomizedTasks = await this.getTasks(); if (!configuredAndCustomizedTasks.some(t => this.taskDefinitionRegistry.compareTasks(t, task))) { - await this.saveTask(sourceFolderUri, { ...task, problemMatcher: [] }); + await this.saveTask(scope, { ...task, problemMatcher: [] }); } try { - await this.taskConfigurationManager.openConfiguration(sourceFolderUri); + await this.taskConfigurationManager.openConfiguration(scope); } catch (e) { - console.error(`Error occurred while opening 'tasks.json' in ${sourceFolderUri}.`, e); + console.error(`Error occurred while opening 'tasks.json' in ${this.taskSourceResolver.resolve(task)}.`, e); } } @@ -359,7 +370,7 @@ export class TaskConfigurations implements Disposable { } /** Writes the task to a config file. Creates a config file if this one does not exist */ - saveTask(sourceFolderUri: string, task: TaskConfiguration): Promise { + saveTask(sourceFolderUri: string, task: TaskConfiguration): Promise { const { _source, $ident, ...preparedTask } = task; const customizedTaskTemplate = this.getTaskCustomizationTemplate(task) || preparedTask; return this.taskConfigurationManager.addTaskConfiguration(sourceFolderUri, customizedTaskTemplate); @@ -389,17 +400,18 @@ export class TaskConfigurations implements Disposable { } }; - for (const [rootFolder, taskConfigs] of this.rawTaskConfigurations.entries()) { + for (const [scopeKey, taskConfigs] of this.rawTaskConfigurations.entries()) { for (const taskConfig of taskConfigs) { + const scope = this.getScopeFromKey(scopeKey); const isValid = this.isTaskConfigValid(taskConfig); if (!isValid) { continue; } - const transformedTask = this.getTransformedRawTask(taskConfig, rootFolder); + const transformedTask = this.getTransformedRawTask(taskConfig, scope); if (this.isDetectedTask(transformedTask)) { - addCustomization(rootFolder, transformedTask); + addCustomization(scopeKey, transformedTask); } else { - addConfiguredTask(rootFolder, transformedTask['label'] as string, transformedTask); + addConfiguredTask(scopeKey, transformedTask['label'] as string, transformedTask); } } } @@ -408,20 +420,20 @@ export class TaskConfigurations implements Disposable { this.tasksMap = newTaskMap; } - private getTransformedRawTask(rawTask: TaskCustomization | TaskConfiguration, rootFolderUri: string): TaskCustomization | TaskConfiguration { + private getTransformedRawTask(rawTask: TaskCustomization | TaskConfiguration, scope: TaskConfigurationScope): TaskCustomization | TaskConfiguration { let taskConfig: TaskCustomization | TaskConfiguration; if (this.isDetectedTask(rawTask)) { const def = this.getTaskDefinition(rawTask); taskConfig = { ...rawTask, _source: def!.source, - _scope: rootFolderUri + _scope: scope }; } else { taskConfig = { ...rawTask, - _source: rootFolderUri, - _scope: rootFolderUri + _source: scope, + _scope: scope }; } return { @@ -447,14 +459,15 @@ export class TaskConfigurations implements Disposable { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async updateTaskConfig(task: TaskConfiguration, update: { [name: string]: any }): Promise { - const sourceFolderUri: string | undefined = this.taskSourceResolver.resolve(task); - if (!sourceFolderUri) { + const scope = task._scope; + if (typeof scope !== 'string') { + // ToDo: configure workspace and user-level scope tasks console.error('Global task cannot be customized'); return; } const configuredAndCustomizedTasks = await this.getTasks(); if (configuredAndCustomizedTasks.some(t => this.taskDefinitionRegistry.compareTasks(t, task))) { // task is already in `tasks.json` - const jsonTasks = this.taskConfigurationManager.getTasks(sourceFolderUri); + const jsonTasks = this.taskConfigurationManager.getTasks(scope); if (jsonTasks) { const ind = jsonTasks.findIndex((t: TaskCustomization | TaskConfiguration) => { if (t.type !== (task.taskType || task.type)) { @@ -471,30 +484,41 @@ export class TaskConfigurations implements Disposable { ...update }; } - this.taskConfigurationManager.setTaskConfigurations(sourceFolderUri, jsonTasks); + this.taskConfigurationManager.setTaskConfigurations(scope, jsonTasks); } else { // task is not in `tasks.json` Object.keys(update).forEach(taskProperty => { task[taskProperty] = update[taskProperty]; }); - this.saveTask(sourceFolderUri, task); + this.saveTask(scope, task); } } - private getSourceFolderFromConfigUri(configFileUri: string): string { - return new URI(configFileUri).parent.parent.path.toString(); + private getKeyFromScope(scope: TaskConfigurationScope): string { + // Converting the enums to string will not yield a valid URI, so the keys will be distinct from any URI. + return scope.toString(); + } + + private getScopeFromKey(key: string): TaskConfigurationScope { + if (TaskScope.Global.toString() === key) { + return TaskScope.Global; + } else if (TaskScope.Workspace.toString() === key) { + return TaskScope.Workspace; + } else { + return key; + } } /** checks if the config is a detected / contributed task */ - private isDetectedTask(task: TaskConfiguration | TaskCustomization): task is ContributedTaskConfiguration { + private isDetectedTask(task: TaskConfiguration | TaskCustomization): boolean { const taskDefinition = this.getTaskDefinition(task); // it is considered as a customization if the task definition registry finds a def for the task configuration return !!taskDefinition; } - private getTaskDefinition(task: TaskConfiguration | TaskCustomization): TaskDefinition | undefined { + private getTaskDefinition(task: TaskCustomization): TaskDefinition | undefined { return this.taskDefinitionRegistry.getDefinition({ ...task, - type: task.taskType || task.type + type: typeof task.taskType === 'string' ? task.taskType : task.type }); } } diff --git a/packages/task/src/browser/task-definition-registry.ts b/packages/task/src/browser/task-definition-registry.ts index 284f16c61e14b..26e7beb927a8b 100644 --- a/packages/task/src/browser/task-definition-registry.ts +++ b/packages/task/src/browser/task-definition-registry.ts @@ -16,8 +16,7 @@ import { injectable } from 'inversify'; import { Event, Emitter } from '@theia/core/lib/common'; -import { TaskConfiguration, TaskCustomization, TaskDefinition } from '../common'; -import URI from '@theia/core/lib/common/uri'; +import { TaskConfiguration, TaskDefinition, TaskCustomization } from '../common'; import { Disposable } from '@theia/core/lib/common/disposable'; @injectable() @@ -115,9 +114,8 @@ export class TaskDefinitionRegistry { } const def = this.getDefinition(one); if (def) { - const oneScope = new URI(one._scope).path.toString(); - const otherScope = new URI(other._scope).path.toString(); - return def.properties.all.every(p => p === 'type' || one[p] === other[p]) && oneScope === otherScope; + // scope is either a string or an enum value. Anyway...the must exactly match + return def.properties.all.every(p => p === 'type' || one[p] === other[p]) && one._scope === other._scope; } return one.label === other.label && one._source === other._source; } diff --git a/packages/task/src/browser/task-folder-preference-provider.ts b/packages/task/src/browser/task-folder-preference-provider.ts deleted file mode 100644 index 90f84d1c492bf..0000000000000 --- a/packages/task/src/browser/task-folder-preference-provider.ts +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -import { injectable } from 'inversify'; -import { FolderPreferenceProvider } from '@theia/preferences/lib/browser/folder-preference-provider'; - -@injectable() -export class TaskFolderPreferenceProvider extends FolderPreferenceProvider { - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected parse(content: string): any { - const tasks = super.parse(content); - if (tasks === undefined) { - return undefined; - } - return { tasks: { ...tasks } }; - } - - protected getPath(preferenceName: string): string[] | undefined { - if (preferenceName === 'tasks') { - return []; - } - if (preferenceName.startsWith('tasks.')) { - return [preferenceName.substr('tasks.'.length)]; - } - return undefined; - } - -} diff --git a/packages/task/src/browser/task-frontend-contribution.ts b/packages/task/src/browser/task-frontend-contribution.ts index a01e44d70db95..744ca3048c9f0 100644 --- a/packages/task/src/browser/task-frontend-contribution.ts +++ b/packages/task/src/browser/task-frontend-contribution.ts @@ -80,6 +80,12 @@ export namespace TaskCommands { label: 'Configure Tasks...' }; + export const TASK_OPEN_USER: Command = { + id: 'task:open_user', + category: TASK_CATEGORY, + label: 'Open User Tasks' + }; + export const TASK_CLEAR_HISTORY: Command = { id: 'task:clear-history', category: TASK_CATEGORY, @@ -282,6 +288,15 @@ export class TaskFrontendContribution implements CommandContribution, MenuContri } ); + registry.registerCommand( + TaskCommands.TASK_OPEN_USER, + { + execute: () => { + this.taskService.openUserTasks(); + } + } + ); + registry.registerCommand( TaskCommands.TASK_CLEAR_HISTORY, { diff --git a/packages/task/src/browser/task-name-resolver.ts b/packages/task/src/browser/task-name-resolver.ts index ae6ad8c3c4567..f6ab62683743d 100644 --- a/packages/task/src/browser/task-name-resolver.ts +++ b/packages/task/src/browser/task-name-resolver.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { TaskConfiguration, ContributedTaskConfiguration } from '../common'; +import { TaskConfiguration } from '../common'; import { TaskDefinitionRegistry } from './task-definition-registry'; import { TaskConfigurations } from './task-configurations'; @@ -49,7 +49,7 @@ export class TaskNameResolver { return task.label || `${task.type}: ${task.task}`; } - private isDetectedTask(task: TaskConfiguration): task is ContributedTaskConfiguration { + private isDetectedTask(task: TaskConfiguration): boolean { return !!this.taskDefinitionRegistry.getDefinition(task); } } diff --git a/packages/task/src/browser/task-preferences.ts b/packages/task/src/browser/task-preferences.ts index 789e68b756edc..90a4ac5576b34 100644 --- a/packages/task/src/browser/task-preferences.ts +++ b/packages/task/src/browser/task-preferences.ts @@ -17,8 +17,6 @@ import { interfaces } from 'inversify'; import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; import { taskSchemaId } from './task-schema-updater'; -import { TaskFolderPreferenceProvider } from './task-folder-preference-provider'; -import { FolderPreferenceProvider } from '@theia/preferences/lib/browser'; import { PreferenceConfiguration } from '@theia/core/lib/browser/preferences/preference-configurations'; export const taskPreferencesSchema: PreferenceSchema = { @@ -37,6 +35,5 @@ export const taskPreferencesSchema: PreferenceSchema = { export function bindTaskPreferences(bind: interfaces.Bind): void { bind(PreferenceContribution).toConstantValue({ schema: taskPreferencesSchema }); - bind(FolderPreferenceProvider).to(TaskFolderPreferenceProvider).inTransientScope().whenTargetNamed('tasks'); bind(PreferenceConfiguration).toConstantValue({ name: 'tasks' }); } diff --git a/packages/task/src/browser/task-schema-updater.ts b/packages/task/src/browser/task-schema-updater.ts index a457a13d04cc1..3fc111b9408d3 100644 --- a/packages/task/src/browser/task-schema-updater.ts +++ b/packages/task/src/browser/task-schema-updater.ts @@ -31,6 +31,7 @@ import URI from '@theia/core/lib/common/uri'; import { ProblemMatcherRegistry } from './task-problem-matcher-registry'; import { TaskDefinitionRegistry } from './task-definition-registry'; import { TaskServer } from '../common'; +import { USER_TASKS_URI } from './task-configurations'; export const taskSchemaId = 'vscode://schemas/tasks'; @@ -86,7 +87,7 @@ export class TaskSchemaUpdater { } catch (e) { this.inmemoryResources.add(taskSchemaUri, schemaContent); this.jsonSchemaStore.registerSchema({ - fileMatch: ['tasks.json'], + fileMatch: ['tasks.json', USER_TASKS_URI.toString()], url: taskSchemaUri.toString() }); } diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index a8434eebe2d2e..62517a2878b40 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -48,7 +48,8 @@ import { DependsOrder, RevealKind, ApplyToKind, - TaskOutputPresentation + TaskOutputPresentation, + TaskConfigurationScope } from '../common'; import { TaskWatcher } from '../common/task-watcher'; import { ProvidedTaskConfigurations } from './provided-task-configurations'; @@ -91,7 +92,7 @@ export class TaskService implements TaskConfigurationClient { /** * The last executed task. */ - protected lastTask: { source: string, taskLabel: string, scope?: string } | undefined = undefined; + protected lastTask: { source: string, taskLabel: string, scope: TaskConfigurationScope } | undefined = undefined; protected cachedRecentTasks: TaskConfiguration[] = []; protected runningTasks = new Map, @@ -408,11 +409,18 @@ export class TaskService implements TaskConfigurationClient { this.cachedRecentTasks = []; } + /** + * Open user ser + */ + openUserTasks(): Promise { + return this.taskConfigurations.openUserTasks(); + } + /** * Returns a task configuration provided by an extension by task source and label. * If there are no task configuration, returns undefined. */ - async getProvidedTask(source: string, label: string, scope?: string): Promise { + async getProvidedTask(source: string, label: string, scope: TaskConfigurationScope): Promise { return this.providedTaskConfigurations.getTask(source, label, scope); } @@ -431,7 +439,7 @@ export class TaskService implements TaskConfigurationClient { * * @returns the last executed task or `undefined`. */ - getLastTask(): { source: string, taskLabel: string, scope?: string } | undefined { + getLastTask(): { source: string, taskLabel: string, scope: TaskConfigurationScope } | undefined { return this.lastTask; } @@ -439,14 +447,14 @@ export class TaskService implements TaskConfigurationClient { * Runs a task, by task configuration label. * Note, it looks for a task configured in tasks.json only. */ - async runConfiguredTask(source: string, taskLabel: string): Promise { - const task = this.taskConfigurations.getTask(source, taskLabel); + async runConfiguredTask(scope: TaskConfigurationScope, taskLabel: string): Promise { + const task = this.taskConfigurations.getTask(scope, taskLabel); if (!task) { this.logger.error(`Can't get task launch configuration for label: ${taskLabel}`); return; } - this.run(source, taskLabel); + this.run(task._source, taskLabel, scope); } /** @@ -464,12 +472,12 @@ export class TaskService implements TaskConfigurationClient { * Runs a task, by the source and label of the task configuration. * It looks for configured and detected tasks. */ - async run(source: string, taskLabel: string, scope?: string): Promise { + async run(source: string, taskLabel: string, scope: TaskConfigurationScope): Promise { let task: TaskConfiguration | undefined; - task = this.taskConfigurations.getTask(source, taskLabel); + task = this.taskConfigurations.getTask(scope, taskLabel); if (!task) { // if a configured task cannot be found, search from detected tasks task = await this.getProvidedTask(source, taskLabel, scope); - if (!task && scope) { // find from the customized detected tasks + if (!task) { // find from the customized detected tasks task = await this.taskConfigurations.getCustomizedTask(scope, taskLabel); } if (!task) { @@ -835,9 +843,10 @@ export class TaskService implements TaskConfigurationClient { this.taskConfigurations.updateTaskConfig(task, update); } - protected async getWorkspaceTasks(workspaceFolderUri: string | undefined): Promise { + protected async getWorkspaceTasks(restrictToFolder: TaskConfigurationScope | undefined): Promise { const tasks = await this.getTasks(); - return tasks.filter(t => t._scope === workspaceFolderUri || t._scope === undefined); + // if we pass undefined, return everything, otherwise only tasks with the same uri or workspace/global scope tasks + return tasks.filter(t => typeof t._scope !== 'string' || t._scope === restrictToFolder); } protected async resolveProblemMatchers(task: TaskConfiguration, customizationObject: TaskCustomization): Promise { @@ -874,7 +883,7 @@ export class TaskService implements TaskConfigurationClient { } protected async getTaskCustomization(task: TaskConfiguration): Promise { - const customizationObject: TaskCustomization = { type: '' }; + const customizationObject: TaskCustomization = { type: '', _scope: task._scope }; const customizationFound = this.taskConfigurations.getCustomizationForTask(task); if (customizationFound) { Object.assign(customizationObject, customizationFound); diff --git a/packages/task/src/browser/task-source-resolver.ts b/packages/task/src/browser/task-source-resolver.ts index 90822ad6fd9ba..407edea9588d7 100644 --- a/packages/task/src/browser/task-source-resolver.ts +++ b/packages/task/src/browser/task-source-resolver.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { TaskConfiguration, ContributedTaskConfiguration } from '../common'; +import { TaskConfiguration, TaskScope } from '../common'; import { TaskDefinitionRegistry } from './task-definition-registry'; @injectable() @@ -26,18 +26,11 @@ export class TaskSourceResolver { /** * Returns task source to display. */ - resolve(task: TaskConfiguration): string | undefined { - const isDetectedTask = this.isDetectedTask(task); - let sourceFolderUri: string | undefined; - if (isDetectedTask) { - sourceFolderUri = task._scope; + resolve(task: TaskConfiguration): string { + if (typeof task._scope === 'string') { + return task._scope; } else { - sourceFolderUri = task._source; + return TaskScope[task._scope]; } - return sourceFolderUri; - } - - private isDetectedTask(task: TaskConfiguration): task is ContributedTaskConfiguration { - return !!this.taskDefinitionRegistry.getDefinition(task); } } diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index 542457e2301c6..1612beae55de4 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -147,15 +147,17 @@ export namespace TaskCustomization { } } +export enum TaskScope { + Workspace, + Global +} + +export type TaskConfigurationScope = string | TaskScope.Workspace | TaskScope.Global; + export interface TaskConfiguration extends TaskCustomization { /** A label that uniquely identifies a task configuration per source */ readonly label: string; - /** - * For a provided task, it is the string representation of the URI where the task is supposed to run from. It is `undefined` for global tasks. - * For a configured task, it is workspace URI that task belongs to. - * This field is not supposed to be used in `tasks.json` - */ - readonly _scope: string | undefined; + readonly _scope: TaskConfigurationScope; } export interface ContributedTaskConfiguration extends TaskConfiguration {