diff --git a/CHANGELOG.md b/CHANGELOG.md index ce5e4bdc4a29d..079882b8b36ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## v1.5.0 +- [tasks] fix vscode.tasks.fetchTasks to no longer filter modified configured tasks aligning to VS Code [#8399](https://github.com/eclipse-theia/theia/pull/8399) - [security] updated version range of `decompress` to fix the known [security vulnerability](https://snyk.io/vuln/SNYK-JS-DECOMPRESS-557358) [#8924](https://github.com/eclipse-theia/theia/pull/8294) - Note: the updated dependency may have a [performance impact](https://github.com/eclipse-theia/theia/pull/7715#issuecomment-667434288) on the deployment of plugins. diff --git a/packages/task/src/browser/task-configurations.spec.ts b/packages/task/src/browser/task-configurations.spec.ts new file mode 100644 index 0000000000000..191cc2c58fda6 --- /dev/null +++ b/packages/task/src/browser/task-configurations.spec.ts @@ -0,0 +1,174 @@ +/******************************************************************************** + * Copyright (C) 2020 SAP SE or an SAP affiliate company 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 {assert} from 'chai'; +import {Container, injectable} from 'inversify'; +import {enableJSDOM} from '@theia/core/lib/browser/test/jsdom'; + +const disableJSDOM = enableJSDOM(); +import {ApplicationProps} from '@theia/application-package/lib/application-props'; +import {FrontendApplicationConfigProvider} from '@theia/core/lib/browser/frontend-application-config-provider'; + +FrontendApplicationConfigProvider.set({ + ...ApplicationProps.DEFAULT.frontend.config +}); +import {TaskConfigurations} from './task-configurations'; +import {TaskConfigurationManager} from './task-configuration-manager'; +import {TaskDefinitionRegistry} from './task-definition-registry'; +import {TaskProvider, TaskProviderRegistry} from './task-contribution'; +import {TaskTemplateSelector} from './task-templates'; +import {TaskSchemaUpdater} from './task-schema-updater'; +import {TaskSourceResolver} from './task-source-resolver'; +import {ProvidedTaskConfigurations} from './provided-task-configurations'; +import {TaskConfiguration, TaskConfigurationScope, TaskCustomization, TaskScope} from '../common/task-protocol'; +import {QuickPickService} from '@theia/core/lib/common/quick-pick-service'; +import {ContributionProvider, Event, ILogger} from '@theia/core/lib/common'; +import { + ApplicationShell, + LabelProviderContribution, + PreferenceProvider, + PreferenceScope, + WidgetManager +} from '@theia/core/lib/browser'; +import {FileService} from '@theia/filesystem/lib/browser/file-service'; +import {WorkspaceService} from '@theia/workspace/lib/browser'; +import {WorkspaceVariableContribution} from '@theia/workspace/lib/browser/workspace-variable-contribution'; +import {Signal} from '@phosphor/signaling'; +import {EditorManager} from '@theia/editor/lib/browser'; +import {PreferenceConfigurations} from '@theia/core/lib/browser/preferences/preference-configurations'; +import {FileChangeType} from '@theia/filesystem/lib/common/files'; +import {IJSONSchema} from '@theia/core/lib/common/json-schema'; +import {MockLogger} from '@theia/core/lib/common/test/mock-logger'; +import {MockPreferenceProvider} from '@theia/core/lib/browser/preferences/test'; + +after(() => disableJSDOM()); + +@injectable() +class MockTaskConfigurationManager extends TaskConfigurationManager { + + changeTasksConfigFire(): void { + this.onDidChangeTaskConfigEmitter.fire({scope: TaskScope.Global, type: FileChangeType.ADDED}); + } + + getTasks(scope: TaskConfigurationScope): (TaskCustomization | TaskConfiguration)[] { + return [ + { + 'type': 'echo', + 'label': 'task c', + 'text': 'Configured task c' + }, + { + 'type': 'echo', + 'label': 'task a', + 'text': 'Detected task a' + } + ]; + } +} + +class MockTaskProvider implements TaskProvider { + async provideTasks(): Promise { + const taskA: TaskConfiguration = { + type: 'echo', + group: 'build', + _scope: '1', + label: 'task a', + text: 'Detected task a' + }; + const taskB: TaskConfiguration = { + type: 'echo', + group: 'build', + _scope: '1', + label: 'task b', + text: 'Detected task b' + }; + return [ + taskA, taskB + ]; + } +} + +describe('getTasks', () => { + + let testContainer: Container; + const taskConfigurationManager = new MockTaskConfigurationManager(); + + it('returns all configured tasks: customized and configured', done => { + const taskConfiguration = testContainer.get(TaskConfigurations); + taskConfigurationManager.changeTasksConfigFire(); + setTimeout(async () => { + const res = await taskConfiguration.getTasks(1); + assert.equal(res.length, 2); + assert.equal(res[0].label, 'task c'); + assert.equal(res[1].label, 'task a'); + done(); + }, 50); + }); + + before(() => { + testContainer = new Container(); + testContainer.bind(ILogger).toDynamicValue(ctx => new MockLogger()); + const workspaceService = new WorkspaceService(); + testContainer.bind(WorkspaceService).toConstantValue(workspaceService); + testContainer.bind(WorkspaceVariableContribution).toSelf().inSingletonScope(); + testContainer.bind(ApplicationShell).toConstantValue({ + currentChanged: new Signal({}), + widgets: () => [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + testContainer.bind(EditorManager).toConstantValue({}); + testContainer.bind(QuickPickService).toConstantValue({}); + const taskDefinitionRegistry = new TaskDefinitionRegistry(); + taskDefinitionRegistry.register({ + taskType: 'echo', + source: 'echotaskprovider', + properties: { + required: ['label', 'text'], + all: ['label', 'text'], + schema: new class implements IJSONSchema { + } + } + }); + testContainer.bind(TaskDefinitionRegistry).toConstantValue(taskDefinitionRegistry); + testContainer.bind(TaskConfigurationManager).toConstantValue(taskConfigurationManager); + + testContainer.bind(ProvidedTaskConfigurations).toSelf(); + testContainer.bind(TaskProviderRegistry).toSelf().inSingletonScope(); + + const taskProviderRegistry = testContainer.get(TaskProviderRegistry); + taskProviderRegistry.register('echo', new MockTaskProvider()); + + testContainer.bind(TaskTemplateSelector).toSelf().inSingletonScope(); + testContainer.bind(TaskSchemaUpdater).toConstantValue({ + onDidChangeTaskSchema: Event.None, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validate(data?: any): boolean { + return true; + } + }); + testContainer.bind(TaskSourceResolver).toConstantValue({}); + testContainer.bind(TaskConfigurations).toSelf().inSingletonScope(); + testContainer.bind(FileService).toConstantValue({}); + testContainer.bind(PreferenceProvider).toDynamicValue(ctx => new MockPreferenceProvider(PreferenceScope.Workspace)).inSingletonScope(); + testContainer.bind(PreferenceConfigurations).toConstantValue({}); + testContainer.bind(WidgetManager).toSelf().inSingletonScope(); + testContainer.bind>(ContributionProvider).toDynamicValue(ctx => ({ + getContributions(): LabelProviderContribution[] { + return []; + } + })).inSingletonScope(); + }); +}); diff --git a/packages/task/src/browser/task-configurations.ts b/packages/task/src/browser/task-configurations.ts index 0de1dfaaa3bae..d273c3b0a7fb9 100644 --- a/packages/task/src/browser/task-configurations.ts +++ b/packages/task/src/browser/task-configurations.ts @@ -130,7 +130,7 @@ export class TaskConfigurations implements Disposable { * The invalid task configs are not returned. */ async getTasks(token: number): Promise { - const configuredTasks = Array.from(this.tasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.values())), [] as TaskConfiguration[]); + const configuredTasksMap = new Map(this.tasksMap); const detectedTasksAsConfigured: TaskConfiguration[] = []; for (const [rootFolder, customizations] of Array.from(this.taskCustomizationMap.entries())) { for (const cus of customizations) { @@ -139,9 +139,19 @@ export class TaskConfigurations implements Disposable { if (detected) { // there might be a provided task that has a different scope from the task we're inspecting detectedTasksAsConfigured.push({ ...detected, ...cus }); + } else { + if (configuredTasksMap.has(rootFolder)) { + configuredTasksMap.get(rootFolder)!.set(cus['label'], cus as TaskConfiguration); + } else { + const newConfigMap = new Map(); + newConfigMap.set(cus['label'], cus as TaskConfiguration); + configuredTasksMap.set(rootFolder, newConfigMap); + } } } } + const configuredTasks = Array.from(configuredTasksMap.values()).reduce((acc, labelConfigMap) => acc.concat(Array.from(labelConfigMap.values())), [] as TaskConfiguration[]); + return [...configuredTasks, ...detectedTasksAsConfigured]; }