Skip to content

Commit

Permalink
enable extensions.json
Browse files Browse the repository at this point in the history
  • Loading branch information
colin-grant-work committed Feb 8, 2021
1 parent 27e6ea7 commit 49130ca
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 17 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/browser/preferences/preference-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ export interface PreferenceInspection<T> {
workspaceFolderValue: T | undefined
}

export type PreferenceInspectionScope = keyof Omit<PreferenceInspection<unknown>, 'preferenceName'>;

/**
* We cannot load providers directly in the case if they depend on `PreferenceService` somehow.
* It allows to load them lazily after DI is configured.
Expand Down
3 changes: 3 additions & 0 deletions packages/vsx-registry/compile.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
},
{
"path": "../plugin-ext-vscode/compile.tsconfig.json"
},
{
"path": "../workspace/compile.tsconfig.json"
}
]
}
1 change: 1 addition & 0 deletions packages/vsx-registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@theia/core": "1.10.0",
"@theia/plugin-ext-vscode": "1.10.0",
"@theia/workspace": "1.10.0",
"@types/bent": "^7.0.1",
"@types/sanitize-html": "^1.13.31",
"@types/showdown": "^1.7.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson 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 { inject, injectable, postConstruct } from 'inversify';
import { InMemoryResources } from '@theia/core';
import { JsonSchemaContribution, JsonSchemaRegisterContext } from '@theia/core/lib/browser/json-schema-store';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
import URI from '@theia/core/lib/common/uri';

export const extensionsSchemaID = 'vscode://schemas/extensions';
const extensionsConfigurationSchema: IJSONSchema = {
$id: extensionsSchemaID,
default: { recommendations: [] },
type: 'object',

properties: {
recommendations: {
title: 'A list of extensions recommended for users of this workspace. Should use the form "<publisher>.<extension name>"',
type: 'array',
items: {
type: 'string',
pattern: '^\\w[\\w-]+\\.\\w[\\w-]+$'
},
default: [],
},
unwantedRecommendations: {
title: 'A list of extensions recommended by default that should not be recommended to users of this workspace. Should use the form "<publisher>.<extension name>"',
type: 'array',
items: {
type: 'string',
pattern: '^\\w[\\w-]+\\.\\w[\\w-]+$'
},
default: [],
}
}
};

@injectable()
export class ExtensionSchemaContribution implements JsonSchemaContribution {
protected readonly uri = new URI(extensionsSchemaID);
@inject(InMemoryResources) protected readonly inmemoryResources: InMemoryResources;

@postConstruct()
protected init(): void {
this.inmemoryResources.add(this.uri, JSON.stringify(extensionsConfigurationSchema));
}

registerSchemas(context: JsonSchemaRegisterContext): void {
context.registerSchema({
fileMatch: ['extensions.json'],
url: this.uri.toString(),
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson 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 { createPreferenceProxy, PreferenceContribution, PreferenceSchema, PreferenceScope, PreferenceService } from '@theia/core/lib/browser';
import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
import { PreferenceConfiguration } from '@theia/core/lib/browser/preferences/preference-configurations';
import { interfaces } from 'inversify';
import { ExtensionSchemaContribution, extensionsSchemaID } from './recommended-extensions-json-schema';

export interface RecommendedExtensions {
recommendations?: string[];
unwantedRecommendations?: string[];
}

export const recommendedExtensionsPreferencesSchema: PreferenceSchema = {
type: 'object',
scope: PreferenceScope.Folder,
properties: {
extensions: {
$ref: extensionsSchemaID,
description: 'A list of the names of extensions recommended for use in this workspace.',
defaultValue: { recommendations: [] },
},
},
};

const IGNORE_RECOMMENDATIONS_ID = 'extensions.ignoreRecommendations';

export const recommendedExtensionNotificationPreferencesSchema: PreferenceSchema = {
type: 'object',
scope: PreferenceScope.Folder,
properties: {
[IGNORE_RECOMMENDATIONS_ID]: {
description: 'Controls whether notifications are shown for extension recommendations.',
default: false,
type: 'boolean'
}
}
};

export const ExtensionNotificationPreferences = Symbol('ExtensionNotificationPreferences');

export function bindExtensionPreferences(bind: interfaces.Bind): void {
bind(ExtensionSchemaContribution).toSelf().inSingletonScope();
bind(JsonSchemaContribution).toService(ExtensionSchemaContribution);
bind(PreferenceContribution).toConstantValue({ schema: recommendedExtensionsPreferencesSchema });
bind(PreferenceConfiguration).toConstantValue({ name: 'extensions' });

bind(ExtensionNotificationPreferences).toDynamicValue(({ container }) => {
const preferenceService = container.get<PreferenceService>(PreferenceService);
return createPreferenceProxy(preferenceService, recommendedExtensionNotificationPreferencesSchema);
}).inSingletonScope();
bind(PreferenceContribution).toConstantValue({ schema: recommendedExtensionNotificationPreferencesSchema });
}
68 changes: 67 additions & 1 deletion packages/vsx-registry/src/browser/vsx-extensions-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import { VSXExtension, VSXExtensionFactory } from './vsx-extension';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { PreferenceInspectionScope, PreferenceService } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { RecommendedExtensions } from './recommended-extensions/recommended-extensions-preference-contribution';
import URI from '@theia/core/lib/common/uri';

@injectable()
export class VSXExtensionsModel {
Expand All @@ -46,6 +50,12 @@ export class VSXExtensionsModel {
@inject(ProgressService)
protected readonly progressService: ProgressService;

@inject(PreferenceService)
protected readonly preferences: PreferenceService;

@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;

@inject(VSXExtensionsSearchModel)
readonly search: VSXExtensionsSearchModel;

Expand All @@ -55,7 +65,8 @@ export class VSXExtensionsModel {
protected async init(): Promise<void> {
await Promise.all([
this.initInstalled(),
this.initSearchResult()
this.initSearchResult(),
this.initRecommended(),
]);
this.initialized.resolve();
}
Expand All @@ -79,6 +90,19 @@ export class VSXExtensionsModel {
}
}

protected async initRecommended(): Promise<void> {
this.preferences.onPreferenceChanged(change => {
if (change.preferenceName === 'extensions') {
this.updateRecommended();
}
});
try {
await this.updateRecommended();
} catch (e) {
console.error(e);
}
}

/**
* single source of all extensions
*/
Expand All @@ -89,11 +113,20 @@ export class VSXExtensionsModel {
return this._installed.values();
}

isInstalled(id: string): boolean {
return this._installed.has(id);
}

protected _searchResult = new Set<string>();
get searchResult(): IterableIterator<string> {
return this._searchResult.values();
}

protected _recommended = new Set<string>();
get recommended(): IterableIterator<string> {
return this._recommended.values();
}

getExtension(id: string): VSXExtension | undefined {
return this.extensions.get(id);
}
Expand Down Expand Up @@ -180,6 +213,39 @@ export class VSXExtensionsModel {
});
}

protected updateRecommended(): Promise<Array<VSXExtension | undefined>> {
return this.doChange<Array<VSXExtension | undefined>>(async () => {
const allRecommendations = new Set<string>();
const allUnwantedRecommendations = new Set<string>();

const updateRecommendationsForScope = (scope: PreferenceInspectionScope, root?: URI) => {
const { recommendations, unwantedRecommendations } = this.getRecommendationsForScope(scope, root);
recommendations.forEach(recommendation => allRecommendations.add(recommendation));
unwantedRecommendations.forEach(unwantedRecommendation => allUnwantedRecommendations.add(unwantedRecommendation));
};

const roots = await this.workspaceService.roots;
for (const root of roots) {
updateRecommendationsForScope('workspaceFolderValue', root.resource);
}
if (this.workspaceService.saved) {
updateRecommendationsForScope('workspaceValue');
}
const recommendedSorted = new Set(Array.from(allRecommendations).sort((a, b) => this.compareExtensions(a, b)).values());
allUnwantedRecommendations.forEach(unwantedRecommendation => recommendedSorted.delete(unwantedRecommendation));
this._recommended = recommendedSorted;
return Promise.all(Array.from(recommendedSorted, plugin => this.refresh(plugin)));
});
}

protected getRecommendationsForScope(scope: PreferenceInspectionScope, root?: URI): Required<RecommendedExtensions> {
const configuredValue = this.preferences.inspect<RecommendedExtensions>('extensions', root?.toString())?.[scope];
return {
recommendations: configuredValue?.recommendations ?? [],
unwantedRecommendations: configuredValue?.unwantedRecommendations ?? [],
};
}

resolve(id: string): Promise<VSXExtension> {
return this.doChange(async () => {
await this.initialized.promise;
Expand Down
9 changes: 9 additions & 0 deletions packages/vsx-registry/src/browser/vsx-extensions-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class VSXExtensionsSourceOptions {
static INSTALLED = 'installed';
static BUILT_IN = 'builtin';
static SEARCH_RESULT = 'searchResult';
static RECOMMENDED = 'recommended';
readonly id: string;
}

Expand All @@ -47,6 +48,11 @@ export class VSXExtensionsSource extends TreeSource {
if (!extension) {
continue;
}
if (this.options.id === VSXExtensionsSourceOptions.RECOMMENDED) {
if (this.model.isInstalled(id)) {
continue;
}
}
if (this.options.id === VSXExtensionsSourceOptions.BUILT_IN) {
if (extension.builtin) {
yield extension;
Expand All @@ -61,6 +67,9 @@ export class VSXExtensionsSource extends TreeSource {
if (this.options.id === VSXExtensionsSourceOptions.SEARCH_RESULT) {
return this.model.searchResult;
}
if (this.options.id === VSXExtensionsSourceOptions.RECOMMENDED) {
return this.model.recommended;
}
return this.model.installed;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
import { injectable, inject, postConstruct } from 'inversify';
import { ViewContainer, PanelLayout, ViewContainerPart, Message } from '@theia/core/lib/browser';
import { VSXExtensionsSearchBar } from './vsx-extensions-search-bar';
import { VSXExtensionsWidget, } from './vsx-extensions-widget';
import { generateExtensionWidgetId } from './vsx-extensions-widget';
import { VSXExtensionsModel } from './vsx-extensions-model';
import { VSXExtensionsSourceOptions } from './vsx-extensions-source';

@injectable()
export class VSXExtensionsViewContainer extends ViewContainer {
Expand Down Expand Up @@ -81,7 +82,8 @@ export class VSXExtensionsViewContainer extends ViewContainer {
}
}
if (this.currentMode === VSXExtensionsViewContainer.SearchResultMode) {
const searchPart = this.getParts().find(part => part.wrapped.id === VSXExtensionsWidget.SEARCH_RESULT_ID);
const searchWidgetId = generateExtensionWidgetId(VSXExtensionsSourceOptions.SEARCH_RESULT);
const searchPart = this.getParts().find(part => part.wrapped.id === searchWidgetId);
if (searchPart) {
searchPart.collapsed = false;
searchPart.show();
Expand All @@ -95,7 +97,8 @@ export class VSXExtensionsViewContainer extends ViewContainer {
}

protected applyModeToPart(part: ViewContainerPart): void {
const partMode = (part.wrapped.id === VSXExtensionsWidget.SEARCH_RESULT_ID ? VSXExtensionsViewContainer.SearchResultMode : VSXExtensionsViewContainer.DefaultMode);
const searchResultWidgetId = generateExtensionWidgetId(VSXExtensionsSourceOptions.SEARCH_RESULT);
const partMode = (part.wrapped.id === searchResultWidgetId ? VSXExtensionsViewContainer.SearchResultMode : VSXExtensionsViewContainer.DefaultMode);
if (this.currentMode === partMode) {
part.show();
} else {
Expand Down
28 changes: 16 additions & 12 deletions packages/vsx-registry/src/browser/vsx-extensions-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import { VSXExtensionsSource, VSXExtensionsSourceOptions } from './vsx-extension

@injectable()
export class VSXExtensionsWidgetOptions extends VSXExtensionsSourceOptions {
title?: string;
}

export const generateExtensionWidgetId = (widgetId: string): string => VSXExtensionsWidget.ID + ':' + widgetId;

@injectable()
export class VSXExtensionsWidget extends SourceTreeWidget {

static ID = 'vsx-extensions';
static INSTALLED_ID = VSXExtensionsWidget.ID + ':' + VSXExtensionsSourceOptions.INSTALLED;
static SEARCH_RESULT_ID = VSXExtensionsWidget.ID + ':' + VSXExtensionsSourceOptions.SEARCH_RESULT;
static BUILT_IN_ID = VSXExtensionsWidget.ID + ':' + VSXExtensionsSourceOptions.BUILT_IN;

static createWidget(parent: interfaces.Container, options: VSXExtensionsWidgetOptions): VSXExtensionsWidget {
const child = SourceTreeWidget.createContainer(parent, {
Expand All @@ -54,8 +54,8 @@ export class VSXExtensionsWidget extends SourceTreeWidget {
super.init();
this.addClass('theia-vsx-extensions');

this.id = VSXExtensionsWidget.ID + ':' + this.options.id;
const title = this.computeTitle();
this.id = generateExtensionWidgetId(this.options.id);
const title = this.options.title ?? this.computeTitle();
this.title.label = title;
this.title.caption = title;

Expand All @@ -64,14 +64,18 @@ export class VSXExtensionsWidget extends SourceTreeWidget {
}

protected computeTitle(): string {
if (this.id === VSXExtensionsWidget.INSTALLED_ID) {
return 'Installed';
}
if (this.id === VSXExtensionsWidget.BUILT_IN_ID) {
return 'Built-in';
switch (this.options.id) {
case VSXExtensionsSourceOptions.INSTALLED:
return 'Installed';
case VSXExtensionsSourceOptions.BUILT_IN:
return 'Built-in';
case VSXExtensionsSourceOptions.RECOMMENDED:
return 'Recommended';
case VSXExtensionsSourceOptions.SEARCH_RESULT:
return 'Open VSX Registry';
default:
return '';
}
return 'Open VSX Registry';
}

}

Loading

0 comments on commit 49130ca

Please sign in to comment.