diff --git a/CHANGELOG.md b/CHANGELOG.md index a3bf971f60ec0..74e3b011e10d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ [1.15.0 Milestone](https://github.com/eclipse-theia/theia/milestone/21) +- [core] add API to filter contributions at runtime [#9317](https://github.com/eclipse-theia/theia/pull/9317) Contributed on behalf of STMicroelectronics - [editor-preview] rewrote `editor-preview`-package classes as extensions of `editor`-package classes [#9518](https://github.com/eclipse-theia/theia/pull/9517) [Breaking Changes:](#breaking_changes_1.15.0) 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..8013922e5eeab --- /dev/null +++ b/examples/api-samples/src/browser/contribution-filter/sample-filtered-command-contribution.ts @@ -0,0 +1,80 @@ +/******************************************************************************** + * 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, FilterContribution, ContributionFilterRegistry, bindContribution, Filter } 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' + }; + + export const FILTERED2: Command = { + id: 'example_command.filtered2', + category: EXAMPLE_CATEGORY, + label: 'This command should be filtered out (2)' + }; +} + +/** + * 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 SampleFilterAndCommandContribution implements FilterContribution, CommandContribution { + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(SampleFilteredCommand.FILTERED2, { execute: () => { } }); + } + + registerContributionFilters(registry: ContributionFilterRegistry): void { + registry.addFilters([CommandContribution], [ + // filter ourselves out + contrib => contrib.constructor !== this.constructor + ]); + registry.addFilters('*', [ + // filter a contribution based on its class name + filterClassName(name => name !== 'SampleFilteredCommandContribution') + ]); + } +} + +export function bindSampleFilteredCommandContribution(bind: interfaces.Bind): void { + bind(CommandContribution).to(SampleFilteredCommandContribution).inSingletonScope(); + bind(SampleFilterAndCommandContribution).toSelf().inSingletonScope(); + bindContribution(bind, SampleFilterAndCommandContribution, [CommandContribution, FilterContribution]); +} + +function filterClassName(filter: Filter): Filter { + return object => { + const className = object?.constructor?.name; + return className + ? filter(className) + : false; + }; +} 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 4b7027b49879c..76cc1bc8bc595 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -101,6 +101,7 @@ import { AuthenticationService, AuthenticationServiceImpl } from '../browser/aut import { DecorationsService, DecorationsServiceImpl } from './decorations-service'; import { keytarServicePath, KeytarService } from '../common/keytar-protocol'; import { CredentialsService, CredentialsServiceImpl } from './credentials-service'; +import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -353,4 +354,6 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo }).inSingletonScope(); bind(CredentialsService).to(CredentialsServiceImpl); + + bind(ContributionFilterRegistry).to(ContributionFilterRegistryImpl).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..7cfefe61d0839 --- /dev/null +++ b/packages/core/src/common/contribution-filter/contribution-filter-registry.ts @@ -0,0 +1,79 @@ +/******************************************************************************** + * 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 { ContributionFilterRegistry, ContributionType, FilterContribution } from './contribution-filter'; +import { Filter } from './filter'; + +/** + * Registry of contribution filters. + * + * Implement/bind to the `FilterContribution` interface/symbol to register your contribution filters. + */ +@injectable() +export class ContributionFilterRegistryImpl implements ContributionFilterRegistry { + + protected initialized = false; + protected genericFilters: Filter[] = []; + protected typeToFilters = new Map[]>(); + + constructor( + @multiInject(FilterContribution) @optional() contributions: FilterContribution[] = [] + ) { + for (const contribution of contributions) { + contribution.registerContributionFilters(this); + } + this.initialized = true; + } + + addFilters(types: '*' | ContributionType[], filters: Filter[]): void { + if (this.initialized) { + throw new Error('cannot add filters after initialization is done.'); + } else if (types === '*') { + this.genericFilters.push(...filters); + } else { + for (const type of types) { + this.getOrCreate(type).push(...filters); + } + } + } + + applyFilters(toFilter: T[], type: ContributionType): T[] { + const filters = this.getFilters(type); + if (filters.length === 0) { + return toFilter; + } + return toFilter.filter( + object => filters.every(filter => filter(object)) + ); + } + + protected getOrCreate(type: ContributionType): Filter[] { + let value = this.typeToFilters.get(type); + if (value === undefined) { + this.typeToFilters.set(type, value = []); + } + return value; + } + + protected getFilters(type: ContributionType): Filter[] { + return [ + ...this.typeToFilters.get(type) || [], + ...this.genericFilters + ]; + } +} + 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..676ccabc88832 --- /dev/null +++ b/packages/core/src/common/contribution-filter/contribution-filter.ts @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ContributionType = interfaces.ServiceIdentifier; + +export const ContributionFilterRegistry = Symbol('ContributionFilterRegistry'); +export interface ContributionFilterRegistry { + + /** + * Add filters to be applied for every type of contribution. + */ + addFilters(types: '*', filters: Filter[]): void; + + /** + * Given a list of contribution types, register filters to apply. + * @param types types for which to register the filters. + */ + addFilters(types: ContributionType[], filters: Filter[]): void; + + /** + * 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[] +} + +export const FilterContribution = Symbol('FilterContribution'); +/** + * Register filters to remove contributions. + */ +export interface FilterContribution { + /** + * Use the registry to register your contribution filters. + */ + registerContributionFilters(registry: ContributionFilterRegistry): void; +} 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..ac660f5930177 --- /dev/null +++ b/packages/core/src/common/contribution-filter/filter.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * 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'); + +/** + * @param toTest Object that should be tested + * @returns `true` if the object passes the test, `false` otherwise. + */ +export type Filter = (toTest: T) => boolean; 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..6907956eb543a --- /dev/null +++ b/packages/core/src/common/contribution-filter/index.ts @@ -0,0 +1,19 @@ +/******************************************************************************** + * 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'; diff --git a/packages/core/src/common/contribution-provider.ts b/packages/core/src/common/contribution-provider.ts index 3a4605e3e4376..94e2b97ee42b6 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'); @@ -38,6 +39,7 @@ class ContainerBasedContributionProvider implements Contributi getContributions(recursive?: boolean): T[] { if (this.services === undefined) { const currentServices: T[] = []; + let filterRegistry: ContributionFilterRegistry | undefined; let currentContainer: interfaces.Container | null = this.container; // eslint-disable-next-line no-null/no-null while (currentContainer !== null) { @@ -48,10 +50,15 @@ class ContainerBasedContributionProvider implements Contributi console.error(error); } } + if (filterRegistry === undefined && currentContainer.isBound(ContributionFilterRegistry)) { + filterRegistry = currentContainer.get(ContributionFilterRegistry); + } // eslint-disable-next-line no-null/no-null currentContainer = recursive === true ? currentContainer.parent : null; } - this.services = currentServices; + + this.services = filterRegistry ? filterRegistry.applyFilters(currentServices, this.serviceIdentifier) : currentServices; + } return this.services; } @@ -73,3 +80,17 @@ export function bindContributionProvider(bindable: Bindable, id: symbol): void { .toDynamicValue(ctx => new ContainerBasedContributionProvider(id, ctx.container)) .inSingletonScope().whenTargetNamed(id); } + +/** + * Helper function to bind a service to a list of contributions easily. + * @param bindable a Container or the bind function directly. + * @param service an already bound service to refer the contributions to. + * @param contributions array of contribution identifiers to bind the service to. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function bindContribution(bindable: Bindable, service: interfaces.ServiceIdentifier, contributions: interfaces.ServiceIdentifier[]): void { + const bind: interfaces.Bind = Bindable.isContainer(bindable) ? bindable.bind.bind(bindable) : bindable; + for (const contribution of contributions) { + bind(contribution).toService(service); + } +} 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 bd65188c42053..812919ab985e3 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -32,6 +32,7 @@ import { QuickPickService, quickPickServicePath } from '../common/quick-pick-ser import { WsRequestValidator, WsRequestValidatorContribution } from './ws-request-validators'; import { KeytarService, keytarServicePath } from '../common/keytar-protocol'; import { KeytarServiceImpl } from './keytar-server'; +import { ContributionFilterRegistry, ContributionFilterRegistryImpl } from '../common/contribution-filter'; decorate(injectable(), ApplicationPackage); @@ -101,4 +102,6 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(ConnectionHandler).toDynamicValue(ctx => new JsonRpcConnectionHandler(keytarServicePath, () => ctx.container.get(KeytarService)) ).inSingletonScope(); + + bind(ContributionFilterRegistry).to(ContributionFilterRegistryImpl).inSingletonScope(); });