diff --git a/CHANGELOG.md b/CHANGELOG.md index f262753da048b..af147fb283c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## v1.13.0 + +- [core] add API to filter contributions at runtime [#9317](https://github.com/eclipse-theia/theia/pull/9317) Contributed on behalf of STMicroelectronics + +[Breaking Changes:](#breaking_changes_1.13.0) + ## v1.12.0 - 3/25/2020 [1.12.0 Milestone](https://github.com/eclipse-theia/theia/milestone/17) diff --git a/examples/api-samples/src/browser/api-samples-frontend-module.ts b/examples/api-samples/src/browser/api-samples-frontend-module.ts index d06b8fb0ea21e..10a3ba15d7ecc 100644 --- a/examples/api-samples/src/browser/api-samples-frontend-module.ts +++ b/examples/api-samples/src/browser/api-samples-frontend-module.ts @@ -16,6 +16,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; import { bindDynamicLabelProvider } from './label/sample-dynamic-label-provider-command-contribution'; +import { bindSampleFilteredCommandContribution } from './contribution-filter/sample-filtered-command-contribution'; import { bindSampleUnclosableView } from './view/sample-unclosable-view-contribution'; import { bindSampleOutputChannelWithSeverity } from './output/sample-output-channel-with-severity'; import { bindSampleMenu } from './menu/sample-menu-contribution'; @@ -31,4 +32,5 @@ export default new ContainerModule(bind => { bindSampleMenu(bind); bindSampleFileWatching(bind); bindVSXCommand(bind); + bindSampleFilteredCommandContribution(bind); }); diff --git a/examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts b/examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts new file mode 100644 index 0000000000000..5ea30b68ec5c9 --- /dev/null +++ b/examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics 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 { Command, CommandContribution, CommandRegistry, ContributionFilter, equals, NameBasedContributionFilter } from '@theia/core/lib/common'; +import { injectable, interfaces } from '@theia/core/shared/inversify'; +export namespace SampleFilteredCommand { + const EXAMPLE_CATEGORY = 'Examples'; + export const FILTERED: Command = { + id: 'example_command.filtered', + category: EXAMPLE_CATEGORY, + label: 'This command should be filtered out' + }; +} + +/** + * This sample command is used to test the runtime filtering of already bound contributions. + */ +@injectable() +export class SampleFilteredCommandContribution implements CommandContribution { + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(SampleFilteredCommand.FILTERED, { execute: () => { } }); + } +} + +@injectable() +export class SampleFilteredCommandContributionFilter extends NameBasedContributionFilter { + + contributions = [CommandContribution]; + doTest(toTest: string): boolean { + return equals(toTest, false, 'SampleFilteredCommandContribution'); + } +} + +export const bindSampleFilteredCommandContribution = (bind: interfaces.Bind) => { + bind(CommandContribution).to(SampleFilteredCommandContribution); + bind(ContributionFilter).to(SampleFilteredCommandContributionFilter); +}; diff --git a/examples/api-tests/src/contribution-filter.spec.js b/examples/api-tests/src/contribution-filter.spec.js new file mode 100644 index 0000000000000..c5267ed4496e4 --- /dev/null +++ b/examples/api-tests/src/contribution-filter.spec.js @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics 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 + ********************************************************************************/ + +// @ts-check +describe('Contribution filter', function () { + this.timeout(5000); + const { assert } = chai; + + const { CommandRegistry, CommandContribution } = require('@theia/core/lib/common/command'); + const { SampleFilteredCommandContribution, SampleFilteredCommand } = require('@theia/api-samples/lib/browser/contribution-filter/sample-filtered-command-contribution'); + + const container = window.theia.container; + const commands = container.get(CommandRegistry); + + it('filtered command in container but not in registry', async function () { + const allCommands = container.getAll(CommandContribution); + assert.isDefined(allCommands.find(contribution => contribution instanceof SampleFilteredCommandContribution), + 'SampleFilteredCommandContribution is not bound in container'); + const filteredCommand = commands.getCommand(SampleFilteredCommand.FILTERED.id); + assert.isUndefined(filteredCommand, 'SampleFilteredCommandContribution should be filtered out but is present in "CommandRegistry"'); + }); + +}); diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 5aad42ea55905..602bd63a59911 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -97,6 +97,7 @@ import { LanguageService } from './language-service'; import { EncodingRegistry } from './encoding-registry'; import { EncodingService } from '../common/encoding-service'; import { AuthenticationService, AuthenticationServiceImpl } from '../browser/authentication-service'; +import { ContributionFilterRegistry } from '../common/contribution-filter'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -342,4 +343,7 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(ContextMenuContext).toSelf().inSingletonScope(); bind(AuthenticationService).to(AuthenticationServiceImpl).inSingletonScope(); + + bind(ContributionFilterRegistry).toSelf().inSingletonScope(); + }); diff --git a/packages/core/src/common/contribution-filter/contribution-filter-registry.ts b/packages/core/src/common/contribution-filter/contribution-filter-registry.ts new file mode 100644 index 0000000000000..417108edf837b --- /dev/null +++ b/packages/core/src/common/contribution-filter/contribution-filter-registry.ts @@ -0,0 +1,69 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics 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, multiInject, optional } from 'inversify'; +import { ContributionFilter, ContributionType } from './contribution-filter'; +import { applyFilters } from './filter'; + +const GENERIC_CONTRIBUTION_FILTER_KEY = '*'; +@injectable() +export class ContributionFilterRegistry { + registry: Map; + constructor(@multiInject(ContributionFilter) @optional() contributionFilters: ContributionFilter[] = []) { + this.registry = new Map(); + contributionFilters.forEach(filter => { + if (!filter.contributions || filter.contributions.length === 0) { + this.addFilter(GENERIC_CONTRIBUTION_FILTER_KEY, filter); + } else { + filter.contributions.forEach(type => { + this.addFilter(type, filter); + }); + } + }); + } + + private addFilter(type: ContributionType, filter: ContributionFilter): void { + this.getOrCreate(type).push(filter); + } + + private getOrCreate(type: ContributionType): ContributionFilter[] { + let value = this.registry.get(type); + if (!value) { + value = []; + this.registry.set(type, value); + } + return value; + } + + get(type: ContributionType): ContributionFilter[] { + const filters = [...(this.registry.get(type) || [])]; + if (type !== GENERIC_CONTRIBUTION_FILTER_KEY) { + filters.push(...(this.registry.get(GENERIC_CONTRIBUTION_FILTER_KEY) || [])); + } + return filters; + } + + /** + * Applies the filters for the given contribution type. Generic filters will be applied on any given type. + * @param toFilter the elements to filter + * @param type the contribution type for which potentially filters were registered + * @returns the filtered elements + */ + applyFilters(toFilter: T[], type: ContributionType): T[] { + const filters = this.get(type); + return applyFilters(toFilter, filters); + } +} diff --git a/packages/core/src/common/contribution-filter/contribution-filter.ts b/packages/core/src/common/contribution-filter/contribution-filter.ts new file mode 100644 index 0000000000000..b423e015a6bbc --- /dev/null +++ b/packages/core/src/common/contribution-filter/contribution-filter.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics 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 { interfaces } from 'inversify'; +import { Filter } from './filter'; +import { NameBasedFilter } from './string-based-filter'; +export type ContributionType = interfaces.ServiceIdentifier; + +export const ContributionFilter = Symbol('ContributionFilter'); + +/** + * Specialized `Filter` that is used by the `ContainerBasedContributionProvider` to + * filter unwanted contributions that are already bound in the DI container. + */ +export interface ContributionFilter extends Filter { + /** + * Contribution types for which this filter is applicable. If `undefined` or empty this filter + * will be applied to all contribution types. + */ + contributions?: ContributionType[]; +} + +/** + * Specialized `ContributionFilter` that can be used to filter contributions based on their constructor name. + */ +export abstract class NameBasedContributionFilter extends NameBasedFilter implements ContributionFilter { +} diff --git a/packages/core/src/common/contribution-filter/filter.ts b/packages/core/src/common/contribution-filter/filter.ts new file mode 100644 index 0000000000000..66eba387fb55c --- /dev/null +++ b/packages/core/src/common/contribution-filter/filter.ts @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics 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 + ********************************************************************************/ + +export const Filter = Symbol('Filter'); +/** + * A `Filter` can be used to test whether a given object should be filtered + * from a set of objects. The `test` function can be applied to an object + * of matching type and returns `true` if the object should be filtered out. + */ +export interface Filter { + /** + * Evaluates this filter on the given argument. + * @param toTest Object that should be tested + * @returns `true` if the object should be filtered out, `false` otherwise + */ + test(toTest: T): Boolean; +} + +/** + * Applies a set of filters to a set of given objects and returns the set of filtered objects. + * @param toFilter Set of objects which should be filtered + * @param filters Set of filters that should be applied + * @param negate Negation flag. If set to true the result of all `Filter.test` methods is negated + * @returns The set of filtered arguments + */ +export function applyFilters(toFilter: T[], filters: Filter[], negate: boolean = false): T[] { + if (filters.length === 0) { + return toFilter; + } + return toFilter.filter(object => { + const result = filters.every(filter => !filter.test(object)); + return negate ? !result : result; + }); +} + diff --git a/packages/core/src/common/contribution-filter/index.ts b/packages/core/src/common/contribution-filter/index.ts new file mode 100644 index 0000000000000..3879774eb5ee2 --- /dev/null +++ b/packages/core/src/common/contribution-filter/index.ts @@ -0,0 +1,20 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics 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 + ********************************************************************************/ + +export * from './contribution-filter'; +export * from './contribution-filter-registry'; +export * from './filter'; +export * from './string-based-filter'; diff --git a/packages/core/src/common/contribution-filter/string-based-filter.ts b/packages/core/src/common/contribution-filter/string-based-filter.ts new file mode 100644 index 0000000000000..c1b47ed593576 --- /dev/null +++ b/packages/core/src/common/contribution-filter/string-based-filter.ts @@ -0,0 +1,106 @@ +/******************************************************************************** + * Copyright (C) 2021 STMicroelectronics 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 { Filter } from './filter'; + +/** + * Specialized `Filter` class for filters based on string level. Test objects + * are converted to a corresponding string representation with the help of the `toString()`-method + * which enable to implement a `doTest()`-method based on string comparison. + */ +@injectable() +export abstract class StringBasedFilter implements Filter { + + /** + * Converts the test object to a corresponding string representation + * that is then used in the `doTest()` method. + * @param toTest Test object that should be converted to string + */ + abstract toString(toTest: T): string | undefined; + + /** + * Tests whether the object should be filtered out based on its string representation. + * @param testStr String representation of the test object + * @returns `true` if the object should be filtered out, `false` otherwise + */ + abstract doTest(testStr: string): boolean; + + test(contribution: T): boolean { + const testStr = this.toString(contribution); + if (typeof testStr !== 'string') { + return false; + } + + return this.doTest(testStr); + } +} +/** + * Utility function to test wether any of the given string patterns is included in the + * tested string. + * @param testStr String that should be tested + * @param ignoreCase Flag, to indicate wether the test should be case-sensitive or not + * @param patterns The set of patterns that should be tested for inclusion + * @returns `true` if any of the given patterns is included in the test string, `false` otherwise + */ +export function includes(testStr: string, ignoreCase: boolean, ...patterns: string[]): boolean { + if (ignoreCase) { + testStr = testStr.toLowerCase(); + patterns = patterns.map(pattern => pattern.toLowerCase()); + } + return patterns.find(pattern => testStr.includes(pattern)) !== undefined; +} + +/** + * Utility function to test wether any of the given string patterns is equal to the + * tested string. + * @param testStr String that should be tested + * @param ignoreCase Flag, to indicate wether the test should be case-sensitive or not + * @param patterns The set of patterns that should be tested for equality + * @returns `true` if any of the given patterns is equal to the test string, `false` otherwise + */ +export function equals(testStr: string, ignoreCase: boolean, ...patterns: string[]): boolean { + if (ignoreCase) { + testStr = testStr.toLowerCase(); + patterns = patterns.map(pattern => pattern.toLowerCase()); + } + return patterns.find(pattern => testStr === pattern) !== undefined; + +} + +/** + * Utility function to test wether a string matches any of the given regular expression patterns. + * @param testStr String that should be tested + * @param ignoreCase Flag, to indicate wether the test should be case-sensitive or not + * @param patterns The set of regular expressions that should be matched + * @returns `true` if the test string matches any of the given regular expressions, `false` otherwise + */ +export function matches(testStr: string, ignoreCase: boolean, ...patterns: (RegExp | string)[]): boolean { + const flags = ignoreCase ? 'i' : undefined; + const regexps = patterns.map(pattern => new RegExp(pattern, flags)); + return regexps.find(regexp => regexp.test(testStr)) !== undefined; +} + +/** + * Specialized `StringBasedFilter` that can be used to filter objects based on their constructor name. + */ +@injectable() +export abstract class NameBasedFilter extends StringBasedFilter { + toString(toTest: T): string { + return toTest.constructor.name; + } +} + diff --git a/packages/core/src/common/contribution-provider.ts b/packages/core/src/common/contribution-provider.ts index 3a4605e3e4376..3ab0cd703b4ba 100644 --- a/packages/core/src/common/contribution-provider.ts +++ b/packages/core/src/common/contribution-provider.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import { interfaces } from 'inversify'; +import { ContributionFilterRegistry } from './contribution-filter'; export const ContributionProvider = Symbol('ContributionProvider'); @@ -29,6 +30,7 @@ export interface ContributionProvider { class ContainerBasedContributionProvider implements ContributionProvider { protected services: T[] | undefined; + protected filterRegistry: ContributionFilterRegistry | undefined; constructor( protected readonly serviceIdentifier: interfaces.ServiceIdentifier, @@ -48,11 +50,17 @@ class ContainerBasedContributionProvider implements Contributi console.error(error); } } + if (!this.filterRegistry && currentContainer.isBound(ContributionFilterRegistry)) { + this.filterRegistry = currentContainer.get(ContributionFilterRegistry); + } // eslint-disable-next-line no-null/no-null currentContainer = recursive === true ? currentContainer.parent : null; } this.services = currentServices; } + if (this.filterRegistry) { + return this.filterRegistry.applyFilters(this.services, this.serviceIdentifier); + } return this.services; } } diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 25f12ec4385a4..7d6fecbd4a480 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -37,6 +37,7 @@ export * from './selection'; export * from './strings'; export * from './application-error'; export * from './lsp-types'; +export * from './contribution-filter'; import { environment } from '@theia/application-package/lib/environment'; export { environment }; diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index 41d43532205d6..8627ea17d80ef 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -30,6 +30,7 @@ import { EnvVariablesServerImpl } from './env-variables'; import { ConnectionContainerModule } from './messaging/connection-container-module'; import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service'; import { WsRequestValidator, WsRequestValidatorContribution } from './ws-request-validators'; +import { ContributionFilterRegistry } from '../common/contribution-filter'; decorate(injectable(), ApplicationPackage); @@ -85,4 +86,6 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(WsRequestValidator).toSelf().inSingletonScope(); bindContributionProvider(bind, WsRequestValidatorContribution); + + bind(ContributionFilterRegistry).toSelf().inSingletonScope(); });