Skip to content

Commit

Permalink
Enable extensions.json
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <[email protected]>
  • Loading branch information
Colin Grant committed Jun 8, 2021
1 parent 2880525 commit 945fa1e
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 76 deletions.
4 changes: 3 additions & 1 deletion packages/core/src/browser/preferences/preference-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ export interface PreferenceInspection<T> {
value: 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 Expand Up @@ -430,7 +432,7 @@ export class PreferenceServiceImpl implements PreferenceService {
if (provider && await provider.setPreference(preferenceName, value, resourceUri)) {
return;
}
throw new Error(`Unable to write to ${PreferenceScope.getScopeNames(resolvedScope)[0]} Settings.`);
throw new Error(`Unable to write to ${PreferenceScope[resolvedScope]} Settings.`);
}

getBoolean(preferenceName: string): boolean | undefined;
Expand Down
70 changes: 42 additions & 28 deletions packages/preferences/src/browser/folders-preferences-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,45 +133,59 @@ export class FoldersPreferencesProvider extends PreferenceProvider {
}

async setPreference(preferenceName: string, value: any, resourceUri?: string): Promise<boolean> {
const sectionName = preferenceName.split('.', 1)[0];
const configName = this.configurations.isSectionName(sectionName) ? sectionName : this.configurations.getConfigName();
const firstPathFragment = preferenceName.split('.', 1)[0];
const defaultConfigName = this.configurations.getConfigName();
const configName = this.configurations.isSectionName(firstPathFragment) ? firstPathFragment : defaultConfigName;

const providers = this.getFolderProviders(resourceUri);
let configPath: string | undefined;

const iterator: (() => FolderPreferenceProvider | undefined)[] = [];
for (const provider of providers) {
if (configPath === undefined) {
const configUri = provider.getConfigUri(resourceUri);
if (configUri) {
configPath = this.configurations.getPath(configUri);
}
const candidates = providers.filter(provider => {
// Attempt to figure out the settings folder (.vscode or .theia) we're interested in.
const containingConfigUri = provider.getConfigUri(resourceUri);
if (configPath === undefined && containingConfigUri) {
configPath = this.configurations.getPath(containingConfigUri);
}
if (this.configurations.getName(provider.getConfigUri()) === configName) {
iterator.push(() => {
if (provider.getConfigUri(resourceUri)) {
return provider;
}
iterator.push(() => {
if (this.configurations.getPath(provider.getConfigUri()) === configPath) {
return provider;
}
iterator.push(() => provider);
});
});
const providerName = this.configurations.getName(containingConfigUri ?? provider.getConfigUri());
return providerName === configName || providerName === defaultConfigName;
});

const configNameAndPathMatches = [];
const configNameOnlyMatches = [];
const configUriMatches = [];
const otherMatches = [];

for (const candidate of candidates) {
const domainMatches = candidate.getConfigUri(resourceUri);
const configUri = domainMatches ?? candidate.getConfigUri();
const nameMatches = this.configurations.getName(configUri) === configName;
const pathMatches = this.configurations.getPath(configUri) === configPath;

// Perfect match, run immediately in case we can bail out early.
if (nameMatches && domainMatches) {
if (await candidate.setPreference(preferenceName, value, resourceUri)) {
return true;
}
} else if (nameMatches && pathMatches) { // Right file in the right folder.
configNameAndPathMatches.push(candidate);
} else if (nameMatches) { // Right file.
configNameOnlyMatches.push(candidate);
} else if (domainMatches) { // Currently valid and governs target URI
configUriMatches.push(candidate);
} else {
otherMatches.push(candidate);
}
}

let next = iterator.shift();
while (next) {
const provider = next();
if (provider) {
if (await provider.setPreference(preferenceName, value, resourceUri)) {
const candidateSets = [configNameAndPathMatches, configNameOnlyMatches, configUriMatches, otherMatches];

for (const candidateSet of candidateSets) {
for (const candidate of candidateSet) {
if (await candidate.setPreference(preferenceName, value, resourceUri)) {
return true;
}
}
next = iterator.shift();
}

return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,24 @@ export class UserConfigsPreferenceProvider extends PreferenceProvider {

async setPreference(preferenceName: string, value: any, resourceUri?: string): Promise<boolean> {
const sectionName = preferenceName.split('.', 1)[0];
const configName = this.configurations.isSectionName(sectionName) ? sectionName : this.configurations.getConfigName();

const providers = this.providers.values();

for (const provider of providers) {
if (this.configurations.getName(provider.getConfigUri()) === configName) {
return provider.setPreference(preferenceName, value, resourceUri);
const defaultConfigName = this.configurations.getConfigName();
const configName = this.configurations.isSectionName(sectionName) ? sectionName : defaultConfigName;

const setWithConfigName = async (name: string): Promise<boolean> => {
for (const provider of this.providers.values()) {
if (this.configurations.getName(provider.getConfigUri()) === name) {
if (await provider.setPreference(preferenceName, value, resourceUri)) {
return true;
}
}
}
return false;
};

if (await setWithConfigName(configName)) { // Try in the section we believe it belongs in.
return true;
} else if (configName !== defaultConfigName) { // Fall back to `settings.json` if that fails.
return setWithConfigName(defaultConfigName);
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ export class WorkspaceFilePreferenceProvider extends AbstractResourcePreferenceP
}

protected getPath(preferenceName: string): string[] {
const firstSegment = preferenceName.split('.')[0];
if (firstSegment && this.configurations.isSectionName(firstSegment)) {
const firstSegment = preferenceName.split('.', 1)[0];
const remainder = preferenceName.slice(firstSegment.length + 1);
if (this.belongsInSection(firstSegment, remainder)) {
// Default to writing sections outside the "settings" object.
const path = [firstSegment];
const pathRemainder = preferenceName.slice(firstSegment.length + 1);
if (pathRemainder) {
path.push(pathRemainder);
if (remainder) {
path.push(remainder);
}
// If the user has already written this section inside the "settings" object, modify it there.
if (this.sectionsInsideSettings.has(firstSegment)) {
Expand All @@ -82,6 +82,10 @@ export class WorkspaceFilePreferenceProvider extends AbstractResourcePreferenceP
return ['settings', preferenceName];
}

protected belongsInSection(firstSegment: string, remainder: string): boolean {
return this.configurations.isSectionName(firstSegment);
}

protected getScope(): PreferenceScope {
return PreferenceScope.Workspace;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/vsx-registry/compile.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
},
{
"path": "../filesystem/compile.tsconfig.json"
},
{
"path": "../preferences/compile.tsconfig.json"
},
{
"path": "../workspace/compile.tsconfig.json"
}
]
}
2 changes: 2 additions & 0 deletions packages/vsx-registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"@theia/filesystem": "1.14.0",
"@theia/plugin-ext": "1.14.0",
"@theia/plugin-ext-vscode": "1.14.0",
"@theia/preferences": "1.14.0",
"@theia/workspace": "1.14.0",
"@types/bent": "^7.0.1",
"@types/dompurify": "^2.0.2",
"@types/sanitize-html": "^2.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/********************************************************************************
* 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 {
FolderPreferenceProvider,
FolderPreferenceProviderFactory,
FolderPreferenceProviderFolder,
UserPreferenceProvider,
UserPreferenceProviderFactory
} from '@theia/preferences/lib/browser';
import { Container, injectable, interfaces } from '@theia/core/shared/inversify';
import { extensionsConfigurationSchema } from './recommended-extensions-json-schema';
import {
WorkspaceFilePreferenceProvider,
WorkspaceFilePreferenceProviderFactory,
WorkspaceFilePreferenceProviderOptions
} from '@theia/preferences/lib/browser/workspace-file-preference-provider';
import { bindFactory } from '@theia/preferences/lib/browser/preference-bindings';
import { SectionPreferenceProviderSection, SectionPreferenceProviderUri } from '@theia/preferences/lib/browser/section-preference-provider';

/**
* The overrides in this file are required because the base preference providers assume that a
* section name (extensions) will not be used as a prefix (extensions.ignoreRecommendations).
*/

@injectable()
export class FolderPreferenceProviderWithExtensions extends FolderPreferenceProvider {
protected getPath(preferenceName: string): string[] | undefined {
const path = super.getPath(preferenceName);
if (this.section !== 'extensions' || !path?.length) {
return path;
}
const isExtensionsField = path[0] in extensionsConfigurationSchema.properties!;
if (isExtensionsField) {
return path;
}
return undefined;
}
}

@injectable()
export class UserPreferenceProviderWithExtensions extends UserPreferenceProvider {
protected getPath(preferenceName: string): string[] | undefined {
const path = super.getPath(preferenceName);
if (this.section !== 'extensions' || !path?.length) {
return path;
}
const isExtensionsField = path[0] in extensionsConfigurationSchema.properties!;
if (isExtensionsField) {
return path;
}
return undefined;
}
}

@injectable()
export class WorkspaceFilePreferenceProviderWithExtensions extends WorkspaceFilePreferenceProvider {
protected belongsInSection(firstSegment: string, remainder: string): boolean {
if (firstSegment === 'extensions') {
return remainder in extensionsConfigurationSchema.properties!;
}
return this.configurations.isSectionName(firstSegment);
}
}

export function bindPreferenceProviderOverrides(bind: interfaces.Bind, unbind: interfaces.Unbind): void {
unbind(UserPreferenceProviderFactory);
unbind(FolderPreferenceProviderFactory);
unbind(WorkspaceFilePreferenceProviderFactory);
bindFactory(bind, UserPreferenceProviderFactory, UserPreferenceProviderWithExtensions, SectionPreferenceProviderUri, SectionPreferenceProviderSection);
bindFactory(
bind,
FolderPreferenceProviderFactory,
FolderPreferenceProviderWithExtensions,
SectionPreferenceProviderUri,
SectionPreferenceProviderSection,
FolderPreferenceProviderFolder,
);
bind(WorkspaceFilePreferenceProviderFactory).toFactory(ctx => (options: WorkspaceFilePreferenceProviderOptions) => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = ctx.container;
child.bind(WorkspaceFilePreferenceProvider).to(WorkspaceFilePreferenceProviderWithExtensions);
child.bind(WorkspaceFilePreferenceProviderOptions).toConstantValue(options);
return child.get(WorkspaceFilePreferenceProvider);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/********************************************************************************
* 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 '@theia/core/shared/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';
import { WorkspaceService } from '@theia/workspace/lib/browser';

export const extensionsSchemaID = 'vscode://schemas/extensions';
export 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-]+$',
patternErrorMessage: "Expected format '${publisher}.${name}'. Example: 'eclipse.theia'."
},
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-]+$',
patternErrorMessage: "Expected format '${publisher}.${name}'. Example: 'eclipse.theia'."
},
default: [],
}
}
};

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

@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(),
});
this.workspaceService.updateSchema('extensions', { $ref: this.uri.toString() });
}
}
Loading

0 comments on commit 945fa1e

Please sign in to comment.