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 committed May 26, 2021
1 parent 583220b commit dd7b941
Show file tree
Hide file tree
Showing 13 changed files with 397 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.14.0 Milestone](https://github.com/eclipse-theia/theia/milestone/20)

- [core] add API to filter contributions at runtime [#9317](https://github.com/eclipse-theia/theia/pull/9317) Contributed on behalf of STMicroelectronics
- [debug] Fix behavior of `Add Configurations` command when empty `launch.json` present. [#9467](https://github.com/eclipse-theia/theia/pull/9467)

<a name="breaking_changes_1.14.0">[Breaking Changes:](#breaking_changes_1.14.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 @@ -98,6 +98,7 @@ import { EncodingRegistry } from './encoding-registry';
import { EncodingService } from '../common/encoding-service';
import { AuthenticationService, AuthenticationServiceImpl } from '../browser/authentication-service';
import { DecorationsService, DecorationsServiceImpl } from './decorations-service';
import { ContributionFilterRegistryImpl } from '../common/contribution-filter';

export { bindResourceProvider, bindMessageService, bindPreferenceService };

Expand Down Expand Up @@ -340,4 +341,6 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo

bind(AuthenticationService).to(AuthenticationServiceImpl).inSingletonScope();
bind(DecorationsService).to(DecorationsServiceImpl).inSingletonScope();

bind(ContributionFilterRegistryImpl).toSelf().inSingletonScope();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/********************************************************************************
* 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 { applyFilters, 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);
}
}
}

/**
* 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[] {
return applyFilters<T>(toFilter, this.getFilters(type));
}

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,46 @@
/********************************************************************************
* 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 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;
}

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;
}
43 changes: 43 additions & 0 deletions packages/core/src/common/contribution-filter/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/********************************************************************************
* 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 should be filtered out, `false` otherwise
*/
export type Filter<T extends Object> = (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 filters is negated
* @returns The set of filtered arguments
*/
export function applyFilters<T extends Object>(toFilter: T[], filters: Filter<T>[], negate: boolean = false): T[] {
if (filters.length === 0) {
return toFilter;
}
return toFilter.filter(
object => filters.every(
// By default we want to *keep* objects when false === filter.test(object)
// because filter.test(object) returns true to exclude items.
filter => negate === filter(object)
)
);
}
20 changes: 20 additions & 0 deletions packages/core/src/common/contribution-filter/index.ts
Original file line number Diff line number Diff line change
@@ -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-utils';
Loading

0 comments on commit dd7b941

Please sign in to comment.