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();
});