Skip to content

Commit

Permalink
Provide API to filter unwanted contributions
Browse files Browse the repository at this point in the history
Adds the ContributionFilterRegistry which can be used to filter
contributions of Theia extensions before they are bound.

This mechanism can be used by application developers to specifically
filter individual contributions from Theia extensions they have no
control over, i.e. core or 3rd party extensions.

Resolves #9069

Contributed on behalf of STMicroelectronics

Signed-off-by: Tobias Ortmayr <tortmayr@eclipsesource.com>
Co-Authored-By: Paul Maréchal <paul.marechal@ericsson.com>

squash me?

Signed-off-by: Paul Maréchal <paul.marechal@ericsson.com>
  • Loading branch information
tortmayr authored and paul-marechal committed Jun 28, 2021
1 parent 210fa4a commit a28c440
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<a name="breaking_changes_1.15.0">[Breaking Changes:](#breaking_changes_1.15.0)</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,4 +32,5 @@ export default new ContainerModule(bind => {
bindSampleMenu(bind);
bindSampleFileWatching(bind);
bindVSXCommand(bind);
bindSampleFilteredCommandContribution(bind);
});
Original file line number Diff line number Diff line change
@@ -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<string>): Filter<Object> {
return object => {
const className = object?.constructor?.name;
return className
? filter(className)
: false;
};
}
36 changes: 36 additions & 0 deletions examples/api-tests/src/contribution-filter.spec.js
Original file line number Diff line number Diff line change
@@ -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"');
});

});
3 changes: 3 additions & 0 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -353,4 +354,6 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
}).inSingletonScope();

bind(CredentialsService).to(CredentialsServiceImpl);

bind(ContributionFilterRegistry).to(ContributionFilterRegistryImpl).inSingletonScope();
});
Original file line number Diff line number Diff line change
@@ -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<Object>[] = [];
protected typeToFilters = new Map<ContributionType, Filter<Object>[]>();

constructor(
@multiInject(FilterContribution) @optional() contributions: FilterContribution[] = []
) {
for (const contribution of contributions) {
contribution.registerContributionFilters(this);
}
this.initialized = true;
}

addFilters(types: '*' | ContributionType[], filters: Filter<Object>[]): 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<T extends Object>(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<Object>[] {
let value = this.typeToFilters.get(type);
if (value === undefined) {
this.typeToFilters.set(type, value = []);
}
return value;
}

protected getFilters(type: ContributionType): Filter<Object>[] {
return [
...this.typeToFilters.get(type) || [],
...this.genericFilters
];
}
}

Original file line number Diff line number Diff line change
@@ -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<any>;

export const ContributionFilterRegistry = Symbol('ContributionFilterRegistry');
export interface ContributionFilterRegistry {

/**
* Add filters to be applied for every type of contribution.
*/
addFilters(types: '*', filters: Filter<Object>[]): 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<Object>[]): 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<T extends Object>(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;
}
23 changes: 23 additions & 0 deletions packages/core/src/common/contribution-filter/filter.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Object> = (toTest: T) => boolean;
19 changes: 19 additions & 0 deletions packages/core/src/common/contribution-filter/index.ts
Original file line number Diff line number Diff line change
@@ -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';
23 changes: 22 additions & 1 deletion packages/core/src/common/contribution-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
********************************************************************************/

import { interfaces } from 'inversify';
import { ContributionFilterRegistry } from './contribution-filter';

export const ContributionProvider = Symbol('ContributionProvider');

Expand All @@ -38,6 +39,7 @@ class ContainerBasedContributionProvider<T extends object> 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) {
Expand All @@ -48,10 +50,15 @@ class ContainerBasedContributionProvider<T extends object> 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;
}
Expand All @@ -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<any>, contributions: interfaces.ServiceIdentifier<any>[]): void {
const bind: interfaces.Bind = Bindable.isContainer(bindable) ? bindable.bind.bind(bindable) : bindable;
for (const contribution of contributions) {
bind(contribution).toService(service);
}
}
1 change: 1 addition & 0 deletions packages/core/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Loading

0 comments on commit a28c440

Please sign in to comment.