Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide API to filter unwanted contributions #9317

Merged
merged 1 commit into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
tortmayr marked this conversation as resolved.
Show resolved Hide resolved
- [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
********************************************************************************/
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved

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) {
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
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);
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved
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